Skip to main content

Dispatcher Caching & Invalidation

Series: AEM Architecture & Operations — Part 1

Updated 9 min readMarkdown
Part of seriesAEM Architecture & Operations
Dispatcher Caching & Invalidation

In the Foundations series we met the Dispatcher as the caching and security layer in front of Publish. In production it is the single biggest factor in how fast your site feels and how much load Publish carries. It's also the source of the most common production complaint in AEM: "I published the change, but the site still shows the old content." This post explains how Dispatcher caching works, how invalidation works, and how to configure and debug both.

How the Dispatcher Cache Works

The Dispatcher cache is file-based. When a request arrives:

  1. Dispatcher checks whether it may cache the request
  2. If a valid cached file exists in its docroot, it serves the file directly and Publish never sees the request
  3. If not, it forwards the request to Publish, and if the response is cacheable it saves the response as a file that mirrors the URL path

So /content/mysite/en/home.html becomes a file at <docroot>/content/mysite/en/home.html.

What Dispatcher Will and Won't Cache

By default, Dispatcher caches a response only when all of these are true:

  • The request is a GET (or HEAD)
  • The URL has a file extension (.html, .json, .css and so on)
  • The URL has no query string (unless you configure /ignoreUrlParams)
  • The response status is 200
  • The request isn't authenticated, unless you enable /allowAuthorized
  • The response doesn't carry headers that forbid caching, such as Dispatcher: no-cache, Cache-Control: no-cache or private, or Pragma: no-cache
  • The path isn't denied by your cache rules

Knowing this list solves a large share of "why isn't this cached?" questions.

The Key Configuration Pieces

In a classic (6.5) Dispatcher farm file, the /cache section holds the important settings:

/cache
  {
  /docroot "/opt/dispatcher/cache"
  /statfileslevel "3"
  /allowAuthorized "0"
  /enableTTL "1"

  /rules
    {
    /0000 { /glob "*" /type "allow" }
    /0001 { /glob "/content/mysite/*/account/*" /type "deny" }
    }

  /invalidate
    {
    /0000 { /glob "*" /type "deny" }
    /0001 { /glob "*.html" /type "allow" }
    }

  /ignoreUrlParams
    {
    /0001 { /glob "*" /type "deny" }
    /0002 { /glob "utm_*" /type "allow" }
    }
  }

What each does:

Setting Purpose
/docroot Where cached files are stored
/statfileslevel How granular invalidation is (explained below)
/rules Which paths and file types may be cached
/invalidate Which cached files become stale when something changes
/ignoreUrlParams Which query parameters can be ignored so the page is still cached
/enableTTL Lets cached files expire based on Cache-Control / Expires headers from Publish
/allowAuthorized Whether authenticated requests may be cached (usually off)

Query Parameters and Cache Hit Ratio

Marketing links like ?utm_source=email make every URL "unique", so each one bypasses the cache. The recommended pattern is an allow-list of ignorable parameters: deny everything, then allow only parameters that never change the content, such as utm_*. In /ignoreUrlParams, an allow rule means "ignore this parameter when deciding whether to cache."

Be careful with the shortcut of allowing *: it would cache one version of a page for every parameter value, including ones that really do change the output (like ?page=2).

How Invalidation Works

Dispatcher doesn't usually delete cached files when content changes. Instead it uses .stat files:

  1. When an author activates a page, a flush request reaches Dispatcher
  2. Dispatcher touches a .stat file at a certain directory level
  3. For any cached file, if the .stat file is newer than the cached file, Dispatcher treats the cached file as stale and fetches a fresh copy from Publish on the next request

Only files that match the /invalidate rules are affected, which is why static files (images, CSS, JS) usually survive an activation while HTML is refreshed.

Understanding /statfileslevel

/statfileslevel decides how much of the cache one activation invalidates. When a file is updated, Dispatcher finds the folder at the configured level and invalidates everything in and below it.

For a typical multi-brand, multi-country, multi-language structure like /content/brand/country/language/...:

/statfileslevel Effect of an activation
0 Invalidates the whole docroot
1 Invalidates per brand (/content/brand)
2 Invalidates per country
3 Invalidates per language

Pick the level that matches how your content tree is organized. Too low a number and a single edit flushes far more than necessary (hurting performance on busy sites). Too high a number and related pages, such as navigation that appears on every page, may not be refreshed when you expect.

Delete vs Invalidate

A flush request carries an action:

  • Activate: invalidate, by touching the .stat file
  • Delete/Deactivate: remove the cached file for the specific path, as well as invalidating

If you want "refresh now" behavior for one resource, a delete flush on that path is the most direct.

Time-Based Invalidation (TTL)

With /enableTTL "1", Dispatcher honors Cache-Control: max-age and Expires headers on responses. A file expires when its TTL passes, even if no activation happened. This is useful for content that changes without being activated, such as feeds, listings or anything driven by external data. Set the headers from Apache (mod_headers) or from your Publish responses.

Who Sends the Flush Request?

On AEM 6.5 or AMS: a Dispatcher Flush replication agent configured on Publish sends an HTTP request to the Dispatcher's /dispatcher/invalidate.cache endpoint after content is replicated. The request carries headers such as CQ-Action, CQ-Handle and Content-Length. The Dispatcher's allowed-clients configuration decides who may send flush requests.

On AEM as a Cloud Service: invalidation is wired up for you. Activating content triggers invalidation automatically, and the Dispatcher configuration you deploy through Cloud Manager includes the flush plumbing. A CDN also sits in front of Dispatcher, so you now have two caches to think about (covered below).

Dispatcher on AEM as a Cloud Service

Cloud Service projects include a dispatcher module based on the Dispatcher SDK. Key differences from a 6.5 setup:

  • The configuration is split into folders such as conf.d (Apache) and conf.dispatcher.d (Dispatcher), with sub-folders for available_farms, cache, filters, clientheaders, renders and virtualhosts
  • Some files are immutable: Adobe controls them, and the validator rejects changes. A common example is default_invalidate.any
  • You validate your configuration locally before deploying, with the SDK's validator
  • You can run Dispatcher locally in Docker to test rules and caching before they reach a real environment

Typical commands (from the SDK folder):

# Validate your configuration
./bin/validator full -d out src

# Run Dispatcher locally against your local AEM Publish
./bin/docker_run.sh out host.docker.internal:4503 8080

Script names and arguments can change between SDK versions, so check the README that ships with your SDK download.

Local testing gotcha: because of the immutable files, a local Dispatcher may not behave exactly like a cloud environment for flush testing. If invalidation looks broken locally (for example, the log says a file was "never flushed" or no .stat files appear), confirm the behavior on a cloud Dev environment before assuming your config is wrong.

The CDN Layer

On Cloud Service, a CDN caches in front of Dispatcher. That means:

  • Cache-Control headers you set matter twice: they influence the CDN and, with TTL enabled, Dispatcher
  • Invalidating Dispatcher does not instantly purge the CDN. A page can be fresh in Dispatcher but stale at the edge until its TTL passes or it is purged
  • Set sensible TTLs per content type (short for HTML, long for versioned clientlibs and images) and plan how you will purge in an emergency. Check Adobe's current documentation for the supported purge options

Debugging Stale Content

When "my change isn't showing", work from the user's browser inward:

  1. Browser: hard refresh, or test in a private window, to rule out browser caching
  2. CDN (Cloud Service): inspect response headers (curl -I https://yoursite/page.html) and look at Age and Cache-Control
  3. Dispatcher: check whether the cached file exists and its timestamp, and compare it with the .stat file at your /statfileslevel folder
  4. Publish: request the page directly from Publish, bypassing Dispatcher. If it's wrong here, the problem is replication, not caching
  5. Author: confirm the page was actually published, and wasn't just saved

Other things to check:

  • Was a flush sent? Look in the flush agent log (6.5) or the Dispatcher log
  • Does the path match an /invalidate rule? A .json or .xml file won't be invalidated if only *.html is allowed
  • Is /statfileslevel too deep or too shallow for the pages that changed? Shared components, like a header, live in a different folder from the page that changed
  • Did Publish send no-cache headers, making files uncacheable in the first place?

Temporarily raise the Dispatcher log level to see per-request decisions (why a request wasn't cached, which .stat file applied). Lower it again afterward, since debug logging is noisy.

Best Practices

  • Cache as much as possible, and treat every uncacheable page as a deliberate decision
  • Pick /statfileslevel to match your content tree, and keep the tree consistent
  • Use an allow-list for ignorable query parameters
  • Keep personalized content out of cached HTML, and load it client-side or with Sling Dynamic Include (SDI)
  • Use versioned clientlibs so static assets can have long TTLs
  • Review /rules and /filter together, since cache rules decide what's stored and filters decide what's reachable at all
  • On Cloud Service, validate locally and test on Dev before promoting Dispatcher changes
  • Monitor your cache hit ratio. A healthy site serves the vast majority of HTML and assets from cache

What's Next

Dispatcher invalidation depends on content actually reaching Publish. In the next post we look at replication, how content moves from Author to Publish on 6.5 and on Cloud Service, and how to troubleshoot it when it doesn't.


Next in this series: [Troubleshooting Replication Agents]

About the author

  • AEM Technical Lead experienced with building and architecting solutions on AEM Sites and Assets. Sharing practical tutorials, real-world implementation lessons, and best practices to help fellow developers and leads navigate AEM.

What's the most stubborn stale-content problem you've hit: Dispatcher, the CDN or something else? Describe what you saw and how you fixed it. Good examples may end up in a future troubleshooting checklist.

AEM Architecture & Operations

Part 1 of 4

"Dispatcher, replication, performance tuning, and cloud vs on-prem decisions — what it takes to run AEM in production."

Up nextTroubleshooting Replication AgentsSeries: AEM Architecture & Operations — Part 2