Performance and caching
This page is about operating Drupal’s caching in production: which cache layer serves which visitor, how to confirm each one is working, and where to put the cache data so a real traffic load stays fast. The metadata that drives all of it (cache tags, contexts, and max-age) is explained on the caching concept page; here you turn the layers on, read their headers, and choose a backend.
Serve most requests from a cache and keep the rest fast, without serving stale or leaked output. By the end you can name which layer handled a given response, confirm it from the response headers, and move the cache storage off the database onto Redis or Memcache when the database becomes the bottleneck. The wrong time to reach for this page is when a specific change is not appearing: that is almost always a missing cache tag, which the caching concept page and Debug a Drupal site cover.
Prerequisites
Section titled “Prerequisites”- A working Drupal 11 site you can send requests to and read response headers from.
- The caching concept page read, so cache tags, contexts, and max-age are familiar.
- Drush for enabling and inspecting modules.
- For the backend section: a Redis or Memcache server reachable from the web nodes, and the matching contributed module installed with Composer.
Drupal layers three HTTP-level caches over the render cache. Each targets a different visitor, and each is a separate module you can confirm and tune.
Understand which layer serves whom
Section titled “Understand which layer serves whom”The layers stack. The internal page cache answers anonymous hits before anything else runs; on a miss, the request continues into Dynamic Page Cache, which serves authenticated visitors too.
- Internal Page Cache (the
page_cachemodule) serves whole HTML pages to anonymous visitors. It runs as an HTTP middleware (http_middleware.page_cache) before the kernel boots fully, so a hit returns in microseconds without routing or rendering. It stores responses keyed by URL and only stores responses that carry cacheability metadata (a\Drupal\Core\Cache\CacheableResponseInterface). It invalidates by cache tag and ignores bubbled max-age: an anonymous page lives until a tag invalidates it. - Dynamic Page Cache (the
dynamic_page_cachemodule) serves pages to authenticated visitors and to anonymous visitors when the internal page cache cannot. It runs as a kernel event subscriber after routing, because the contexts a page varies by (user,user.permissions,route) can only be known then. It stores each page in a\Drupal\Core\Cache\VariationCacheunder the contexts the page declares, so one logical page has one cached variant per context combination. Uncacheable fragments are replaced with placeholders before the page is stored, so the cacheable shell is reused while the personalized parts are rebuilt per request. - BigPipe (the
big_pipemodule) is not a separate store; it is a delivery strategy for the placeholders Dynamic Page Cache leaves behind. It flushes the cacheable page shell to the browser immediately, then streams each placeholder’s rendered content as it finishes. The visitor sees the page frame at once instead of waiting for the slowest fragment.
Confirm which modules are active:
ddev drush pm:list --status=enabled --type=module | grep -E 'page_cache|dynamic_page_cache|big_pipe'page_cache, dynamic_page_cache, and big_pipe are all installed by the standard profile. Leave them enabled in production. The internal page cache is the single largest performance win for anonymous traffic.
Read the cache headers
Section titled “Read the cache headers”Every page response reports how each cache layer treated it. Request a page and inspect the headers.
curl -sI https://example.com/ | grep -i x-drupalX-Drupal-Cache: HITX-Drupal-Dynamic-Cache: MISSX-Drupal-Cache-Tags: block_view config:block_list ...X-Drupal-Cache-Contexts: languages:language_interface theme url.site ...X-Drupal-Cacheis the internal page cache (anonymous only):HIT,MISS, orUNCACHEABLEwith a reason. The reasons come straight from the middleware:UNCACHEABLE (request policy)for a request with an open session,UNCACHEABLE (no cacheability)for a response that is not aCacheableResponseInterface,UNCACHEABLE (response policy)when a policy denied it.X-Drupal-Dynamic-Cacheis Dynamic Page Cache, with the sameHIT/MISS/UNCACHEABLEvocabulary.UNCACHEABLE (poor cacheability)means the page declared a high-cardinality context or a high-frequency tag that makes whole-page caching pointless.X-Drupal-Cache-TagsandX-Drupal-Cache-Contextsare emitted on debug-enabled environments and list what the response depends on and varies by. They are how you find a missing tag.
The cacheability headers are gated behind the http.response.debug_cacheability_headers container parameter; enable it in services.yml on non-production environments only, since the tag list can be large. Debug a Drupal site walks through reading them.
Force a page out of cache when you must
Section titled “Force a page out of cache when you must”Code that produces genuinely uncacheable output flips the page-cache kill switch. It marks the current response uncacheable for both page caches for this request only.
// Fragment: $this->killSwitch is the page_cache_kill_switch service.$this->killSwitch->trigger();Trigger it only when no tag, context, or max-age can describe the variation, because it disables whole-page caching for that request. The \Drupal\Core\PageCache\ResponsePolicy\KillSwitch response policy reads the flag and denies caching. Prefer correct metadata; reach for the kill switch last.
Move the cache backend off the database
Section titled “Move the cache backend off the database”By default the cache bins are stored in the database (cache.backend.database). Under load, cache reads and writes contend with content queries on the same database. Moving the high-traffic bins to an in-memory store removes that contention.
Install the Redis or Memcache contributed module with Composer and enable it, then point cache bins at its backend in settings.php. The pattern is the same for either; this makes Redis the default backend:
// settings.php (the redis module is installed and enabled).$settings['redis.connection']['host'] = '127.0.0.1';$settings['redis.connection']['port'] = 6379;$settings['cache']['default'] = 'cache.backend.redis';$settings['container_yamls'][] = 'modules/contrib/redis/example.services.yml';The cache.backend.redis service exists only once the module is enabled, so add these lines after enabling it. The container_yamls line moves cache tag checksums, locks, and flood control to Redis as well; without it, every cache read still queries the cachetags table in the database. The module’s settings.redis.example.php and README cover the full set of options.
$settings['cache']['default'] sets the backend for every bin that does not declare its own default; $settings['cache']['bins']['render'] = 'cache.backend.redis' overrides a single bin. Each bin is a service named cache.BIN implementing \Drupal\Core\Cache\CacheBackendInterface, so swapping the backend is a settings change with no code change. The render and dynamic-page-cache data are the bins that benefit most. The bootstrap, config, and discovery bins are small and read constantly, so core already runs them on cache.backend.chainedfast, which keeps a per-server APCu copy in front of the default backend: with Redis as the default, they become APCu in front of Redis with no extra setting.
Put a reverse proxy in front
Section titled “Put a reverse proxy in front”A reverse proxy (Varnish, or a content delivery network) caches whole anonymous responses outside PHP entirely, so a hit never reaches Drupal. When Drupal sits behind a proxy or load balancer, tell it so, or it will see the proxy’s IP and the wrong scheme on every request.
// settings.php (Drupal is behind a trusted reverse proxy).$settings['reverse_proxy'] = TRUE;$settings['reverse_proxy_addresses'] = ['192.0.2.10'];With reverse_proxy set, Drupal reads the client IP, host, and protocol from the forwarded headers sent by the addresses you list, which keeps ip and url cache contexts, logging, and HTTPS detection correct. The proxy itself caches by URL and respects the Cache-Control and Vary headers Drupal sets; a tag-aware proxy integration can additionally purge by cache tag when content changes, the same invalidation the internal page cache uses. List only proxy addresses you control: these settings make Drupal trust forwarded headers, and trusting an attacker’s headers is a spoofing hole. The security checklist covers the host-header side of the same concern.
Verify
Section titled “Verify”- Anonymous caching works: a second
curl -sI https://example.com/showsX-Drupal-Cache: HIT. - Authenticated caching works: with a session cookie, a repeated request shows
X-Drupal-Dynamic-Cache: HIT. - BigPipe streams: load a page with placeholders (the admin toolbar’s user fragment, for example) in a browser and watch the shell paint before the personalized parts fill in.
- The backend moved: after setting
cache.backend.redis,vendor/bin/drush cache:rebuildsucceeds andredis-cli dbsizegrows, confirming bins are landing in Redis rather than the database. - The proxy is trusted: behind the proxy, logged requests show real client IPs, not the proxy’s address.
Variations
Section titled “Variations”Vary the backend per bin
Section titled “Vary the backend per bin”Instead of changing the default, move only the large bins to the shared store: $settings['cache']['bins']['render'] = 'cache.backend.redis' (and the same for dynamic_page_cache and page) leaves every other bin in the database. Core’s chainedfast bins keep their APCu layer either way.
Tune the 404 cache
Section titled “Tune the 404 cache”Anonymous 404 and 403 responses are cached for an hour by default to keep them from filling the cache, controlled by $settings['cache_ttl_4xx']. Lower it on sites that generate many distinct missing URLs; set it to 0 to stop caching client errors entirely.
Related
Section titled “Related”- Caching for the cache tags, contexts, and max-age that decide whether any of these layers can store a response.
- Debug a Drupal site for reading the
X-Drupal-Cache-TagsandX-Drupal-Cache-Contextsheaders and finding a missing tag. - Deployment workflow for rebuilding a shared cache backend safely across multiple web nodes.
- The
Cache API topicon api.drupal.org for the bin and backend reference.
Was this page helpful?
Drupal is a registered trademark of Dries Buytaert.