Skip to main content

Dispatcher Configuration

Introduction

The AEM Dispatcher is an Apache HTTP Server (httpd) module that sits between the CDN/load balancer and the AEM Publish instances. It serves two critical functions:

  1. Caching — stores rendered pages and static assets on the web server's filesystem so that subsequent requests are served directly from disk, bypassing the publish instance entirely.
  2. Security filtering — acts as a request-level firewall that blocks access to internal paths, admin consoles, and unintended URL patterns before they ever reach AEM.

In an AEM as a Cloud Service (AEMaaCS) topology, every request to a Publish tier flows through the Dispatcher:

Client → CDN → Dispatcher (Apache httpd + mod_dispatcher) → AEM Publish

A well-tuned Dispatcher configuration can reduce publish-tier load by 80–95 %, dramatically improve response times, and harden the attack surface of your AEM installation.

tip

In AEMaaCS the Dispatcher configuration ships as part of your Git repository and is validated on every Cloud Manager pipeline run. Always test locally with the Dispatcher SDK before pushing.


Project Directory Structure

The AEMaaCS SDK ships a standard Dispatcher directory layout under dispatcher/src/. Understanding this tree is essential before making any changes.

dispatcher/src/
├── conf.d/ # Apache httpd configuration
│ ├── available_vhosts/
│ │ └── default.vhost # Default virtual host definition
│ ├── enabled_vhosts/
│ │ └── default.vhost -> ../available_vhosts/default.vhost
│ ├── dispatcher_vhost.conf # Main vhost include file
│ ├── rewrites/
│ │ ├── rewrite.rules # Custom Apache mod_rewrite rules
│ │ └── xforwarded_forcessl_rewrite.rules
│ └── variables/
│ ├── custom.vars # Project-specific variables
│ └── global.vars # Global environment variables
├── conf.dispatcher.d/ # Dispatcher module configuration
│ ├── available_farms/
│ │ └── default.farm # Default farm definition
│ ├── enabled_farms/
│ │ └── default.farm -> ../available_farms/default.farm
│ ├── cache/
│ │ ├── rules.any # Cache rules (what to cache)
│ │ └── invalidate.any # Auto-invalidation rules
│ ├── clientheaders/
│ │ └── clientheaders.any # Forwarded client headers
│ ├── filters/
│ │ └── filters.any # Request filter rules
│ ├── renders/
│ │ └── renders.any # Backend publish host definitions
│ └── virtualhosts/
│ └── virtualhosts.any # Dispatcher-level virtual host matching
└── opt-in/
└── USE_SOURCES_DIRECTLY # Marker file for SDK opt-in
DirectoryPurposeKey files
conf.d/Standard Apache httpd directives — virtual hosts, rewrite rules, environment variablesrewrite.rules, default.vhost, custom.vars
conf.dispatcher.d/Dispatcher module directives — farms, filters, cache, headersfilters.any, rules.any, default.farm
opt-in/Feature flags consumed by the Dispatcher SDK toolingUSE_SOURCES_DIRECTLY
warning

Do not rename or restructure the top-level directories. The Cloud Manager pipeline validator expects exactly this layout and will reject the deployment if files are missing or misplaced.


Farm Configuration

A farm is the Dispatcher's primary configuration unit. It binds incoming requests to a set of backend renders, filters, cache rules, and headers. The entry point is the farm file (e.g., default.farm), which includes the other .any files.

# conf.dispatcher.d/available_farms/default.farm

/default {
/clientheaders {
$include "../clientheaders/clientheaders.any"
}

/virtualhosts {
$include "../virtualhosts/virtualhosts.any"
}

/renders {
$include "../renders/renders.any"
}

/filter {
$include "../filters/filters.any"
}

/cache {
/docroot "/mnt/var/www/html"
/statfileslevel "2"
/allowAuthorized "0"
/rules {
$include "../cache/rules.any"
}
/invalidate {
$include "../cache/invalidate.any"
}
}

/vanity_urls {
/url "/libs/granite/dispatcher/content/vanityUrls.html"
/file "/tmp/vanity_urls"
/delay 300
}
}

Section Reference

SectionPurpose
/clientheadersDefines which HTTP headers from the client are forwarded to the publish instance
/virtualhostsRestricts which Host header values this farm responds to
/rendersBackend publish hosts (hostname + port)
/filterRequest allow/deny rules evaluated top-to-bottom
/cacheCaching behaviour: document root, cache rules, invalidation, TTL
/vanity_urlsEnables AEM vanity URL resolution at the Dispatcher level

Renders

The /renders section tells the Dispatcher where to forward cache-miss requests:

# conf.dispatcher.d/renders/renders.any

/rend01 {
/hostname "localhost"
/port "4503"
/timeout "10000"
}

In AEMaaCS, the hostname and port are replaced at deployment time by environment-specific values — you should keep localhost:4503 for local SDK testing.


Filter Rules

Filter rules are evaluated top-to-bottom. The first matching rule wins. Always start with a global deny and then selectively allow known paths.

# conf.dispatcher.d/filters/filters.any

# Deny everything by default
/0001 { /type "deny" /url "*" }

# Allow content pages
/0002 { /type "allow" /url "/content/*" }

# Allow client libraries
/0003 { /type "allow" /url "/etc.clientlibs/*" }

# Allow static media
/0004 { /type "allow" /url "/content/dam/*" }

# Allow GraphQL persisted queries
/0005 { /type "allow" /url "/graphql/execute.json/*" }

# Allow Experience Fragments
/0006 { /type "allow" /url "/content/experience-fragments/*" }

# Deny access to admin and system paths
/0100 { /type "deny" /url "/admin/*" }
/0101 { /type "deny" /url "/system/*" }
/0102 { /type "deny" /url "/apps/*" }
/0103 { /type "deny" /url "/bin/*" }
/0104 { /type "deny" /url "/crx/*" }
/0105 { /type "deny" /url "/libs/*" }
/0106 { /type "deny" /url "/etc/*" }

# Deny query strings that reveal selectors or suffixes
/0200 { /type "deny" /method "GET" /query "debug=*" }
/0201 { /type "deny" /method "GET" /query "wcmmode=*" }

Common Paths to Allow or Deny

PathActionReason
/content/*AllowPublished site content
/etc.clientlibs/*AllowVersioned client-side JS/CSS bundles
/content/dam/*AllowDigital assets (images, PDFs, videos)
/graphql/execute.json/*AllowContent Fragment GraphQL endpoint
/libs/*DenyInternal server-side libraries
/apps/*DenyApplication code — never exposed publicly
/system/*DenyAEM system servlets
/crx/*DenyCRX/DE repository browser
/bin/*DenyInternal servlets and workflow triggers
warning

Overly permissive filter rules (e.g., /0002 { /type "allow" /url "/*" }) are the number-one cause of Dispatcher security incidents. Always deny by default and only allow explicit, known paths.


Cache Rules

Cache rules determine which response types are stored on disk and served for subsequent requests.

What to Cache

# conf.dispatcher.d/cache/rules.any

# Cache HTML pages
/0000 { /glob "*" /type "deny" }
/0001 { /glob "*.html" /type "allow" }
/0002 { /glob "*.css" /type "allow" }
/0003 { /glob "*.js" /type "allow" }
/0004 { /glob "*.json" /type "allow" }
/0005 { /glob "*.png" /type "allow" }
/0006 { /glob "*.jpg" /type "allow" }
/0007 { /glob "*.jpeg" /type "allow" }
/0008 { /glob "*.gif" /type "allow" }
/0009 { /glob "*.svg" /type "allow" }
/0010 { /glob "*.ico" /type "allow" }
/0011 { /glob "*.woff2" /type "allow" }
/0012 { /glob "*.webp" /type "allow" }

Auto-Invalidation Rules

Auto-invalidation tells the Dispatcher which cached files to mark stale when a flush event is received from AEM:

# conf.dispatcher.d/cache/invalidate.any

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

When AEM publishes a page, the replication agent sends a flush request. The Dispatcher then touches the .stat file in the corresponding directory, which marks all sibling cached files as stale (they will be re-fetched on the next request).

Grace Period

You can configure a grace period so that stale content is served while the Dispatcher re-fetches in the background:

/cache {
/gracePeriod "2"
/enableTTL "1"
}
tip

A grace period of 2 seconds is usually sufficient to prevent a thundering-herd effect when a popular page is invalidated and dozens of requests arrive simultaneously.


Rewrite Rules

Apache mod_rewrite lets you shorten internal AEM paths to clean, SEO-friendly URLs. These rules live in conf.d/rewrites/rewrite.rules.

Basic Content Path Shortening

Strip /content/mysite from URLs so that /content/mysite/en/products.html becomes /en/products.html:

# conf.d/rewrites/rewrite.rules

RewriteEngine On

# Shorten /content/mysite to root
RewriteRule ^/content/mysite/(.*)$ /$1 [PT,L]

# Redirect old URLs to shortened form (301)
RewriteCond %{REQUEST_URI} ^/content/mysite/(.*)$
RewriteRule ^/content/mysite/(.*)$ /$1 [R=301,L]

Append .html Extension

If your site omits .html in navigation links, add it transparently:

# If the request has no extension and is not a known asset path, append .html
RewriteCond %{REQUEST_URI} !\..*$
RewriteCond %{REQUEST_URI} !^/(etc|content/dam|libs|graphql)
RewriteRule ^/(.*)$ /content/mysite/$1.html [PT,L]

Trailing Slash Normalisation

# Remove trailing slash (except root)
RewriteCond %{REQUEST_URI} ^(.+)/$
RewriteRule ^(.+)/$ $1 [R=301,L]
tip

Always test rewrite rules locally with curl -v against your Docker-based Dispatcher before pushing to Cloud Manager. Redirect loops are surprisingly easy to create.


Cache Invalidation

Understanding the .stat file mechanism is critical for keeping content fresh without sacrificing performance.

How .stat Files Work

When the Dispatcher receives a flush request (typically from an AEM replication flush agent), it:

  1. Locates the cache directory for the flushed path.
  2. Creates or updates a hidden .stat file in that directory.
  3. On the next request, the Dispatcher compares the cached file's mtime against the .stat file's mtime. If the .stat file is newer, the cached file is considered stale and is re-fetched from publish.

statfileslevel

The statfileslevel setting controls how deep in the directory tree .stat files are created:

/cache {
/statfileslevel "2"
}
LevelBehaviourTrade-off
"0"Single .stat at the docroot — every flush invalidates the entire cacheSimple but aggressive
"1".stat per first-level directory (e.g., /content/mysite/)Balanced for single-site setups
"2".stat per second-level directory (e.g., /content/mysite/en/)Good for multi-language sites
"3"+Deeper granularity — only nearby cached files are invalidatedPrecise but more .stat file overhead
tip

For a typical multi-language site, statfileslevel "2" is a good starting point. It ensures that flushing an English page does not invalidate cached German pages.

Flush Agent Configuration

In AEMaaCS, flush agents are pre-configured. For on-premise or AMS environments, set up a flush agent on the Author instance:

Agent: Dispatcher Flush
Transport URI: http://dispatcher-host:80/dispatcher/invalidate.cache
HTTP Method: GET
HTTP Headers:
CQ-Action: {action}
CQ-Handle: {path}
CQ-Path: {path}
Triggers: On Receive (replication events)

TTL Caching

Beyond the .stat-based invalidation model, the Dispatcher supports time-to-live (TTL) caching driven by HTTP Cache-Control headers.

Enable TTL

/cache {
/enableTTL "1"
}

When TTL is enabled, the Dispatcher respects the max-age or s-maxage directive in the response's Cache-Control header. A cached file whose age exceeds the TTL is re-fetched regardless of the .stat file.

Setting Cache-Control Headers in AEM

Use a Sling Rewriter or an OSGi configuration to inject headers:

// Example: Sling Filter that sets Cache-Control
@Component(service = Filter.class,
property = {
"sling.filter.scope=REQUEST",
"service.ranking:Integer=700"
})
public class CacheControlFilter implements Filter {

@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
chain.doFilter(request, response);
if (response instanceof HttpServletResponse httpResp) {
httpResp.setHeader("Cache-Control", "max-age=300, s-maxage=3600");
}
}
}
HeaderWho obeys itTypical value
max-ageBrowser + CDN + Dispatcher (if enableTTL "1")300 (5 min for HTML)
s-maxageCDN + Dispatcher only (overrides max-age)3600 (1 hour)
stale-while-revalidateCDN (serves stale while re-fetching)60

CDN Integration

In AEMaaCS, a CDN (Fastly) sits in front of the Dispatcher. The Dispatcher's Cache-Control headers flow through to the CDN, so a single header controls both layers:

Cache-Control: public, max-age=300, s-maxage=3600, stale-while-revalidate=60
warning

Never set long TTLs on personalized or authenticated content. Doing so caches user-specific data and serves it to other visitors. Use Cache-Control: private, no-store for such responses.


Local Testing with Dispatcher SDK

Adobe provides a Docker-based Dispatcher SDK so you can validate your configuration before deploying.

Prerequisites

  • Docker Desktop
  • AEM as a Cloud Service SDK (download from the Software Distribution portal)
  • A running local AEM Publish instance on port 4503

Running the Dispatcher Locally

# Extract the SDK
unzip aem-sdk-dispatcher-tools-*.zip -d dispatcher-sdk

# Validate the configuration
cd dispatcher-sdk
./bin/validator full dispatcher/src

# Run Dispatcher in Docker (forwards to local Publish on port 4503)
./bin/docker_run.sh dispatcher/src host.docker.internal:4503 8080

After the container starts, your site is available at http://localhost:8080. All Dispatcher caching, filtering, and rewrite rules are active.

Validation Commands

CommandPurpose
./bin/validator full dispatcher/srcFull validation of all Dispatcher configs (same checks as Cloud Manager)
./bin/validator httpd dispatcher/srcValidate Apache httpd configs only
./bin/validator dispatcher dispatcher/srcValidate Dispatcher module configs only
./bin/docker_run.sh src host.docker.internal:4503 8080Start Dispatcher container on port 8080
docker logs -f <container_id>Tail Dispatcher/Apache logs in real time

Debugging Tips

Inspect the Dispatcher cache on disk inside the container:

# Enter the running container
docker exec -it <container_id> /bin/bash

# Check the cache directory
ls -la /mnt/var/www/html/content/mysite/en/

# View the .stat file timestamps
stat /mnt/var/www/html/content/mysite/.stat
tip

Run the validator as a Git pre-commit hook so that broken configurations never reach your remote branch:

#!/bin/sh
# .git/hooks/pre-commit
./dispatcher-sdk/bin/validator full dispatcher/src || exit 1

Common Pitfalls

1. Overly Broad Allow Rules

# BAD — allows everything, defeating the purpose of the filter
/0001 { /type "deny" /url "*" }
/0002 { /type "allow" /url "/*" }

Instead, enumerate only the paths your site actually needs.

2. Caching Personalised Content

If a page contains user-specific components (shopping cart, logged-in greeting), it must not be cached, or you risk serving one user's data to another:

# Exclude personalised paths from caching
/nocache01 { /glob "/content/mysite/*/user-dashboard*" /type "deny" }

Also set Cache-Control: private, no-store in the response headers.

3. Missing /etc.clientlibs Rule

Forgetting to allow /etc.clientlibs/* causes all JavaScript and CSS to fail on the published site. This is one of the most common first-time Dispatcher issues:

# Always include this
/0003 { /type "allow" /url "/etc.clientlibs/*" }

4. Forgetting to Invalidate

After deploying content changes, verify that your flush agents are active and that .stat files are being updated. A stale cache is invisible — the site looks fine but shows old content.

5. Not Testing with the Dispatcher During Development

Many issues (broken links, missing assets, redirect loops) only surface when the Dispatcher is in the request path. Run the Docker-based Dispatcher locally as part of your daily development workflow, not just before deployment.

6. Wrong statfileslevel

Setting statfileslevel "0" in a multi-site installation means every flush nukes the entire cache across all sites. Review the level carefully based on your content tree depth.

warning

Always run ./bin/validator full before committing. Cloud Manager will reject deployments with invalid Dispatcher configs, and a broken pipeline blocks all teams from deploying.


See also