Drupal caching reuses previously generated output instead of rebuilding it for every request. Internal Page Cache stores complete pages for anonymous visitors; Dynamic Page Cache reuses pages while leaving personalized parts dynamic; Render Cache reuses individual pieces. Cache tags say what data an item depends on, cache contexts say what it varies by, and max-age says how long it may be cached. Correct metadata makes pages faster without showing stale or someone else's content.

I have been working with Drupal as a CMS for years, both as a backend Drupal developer and as the person paged at 2am when a homepage refuses to update. This post is the guide I wish I had when I started: how the Drupal cache actually works, where it breaks, and the practical patterns I use to keep it honest.

What Is Drupal Page Caching?

Page caching stores reusable page responses, reducing rendering work on subsequent requests. Actual performance depends on your application, cache backend and hosting, so measure before and after a change rather than expecting a fixed response time.

There are two main built-in modules involved: **Internal Page Cache** for anonymous users and **Internal Dynamic Page Cache** for anonymous and authenticated users. They solve different problems and can be enabled together.

The Four Layers of the Drupal Cache

Most developers think of "the cache" as a single thing. In reality, a Drupal request touches several caches before it returns a byte to the browser.

### 1. Internal Page Cache This layer stores complete responses for anonymous visitors and can return a hit early in the request. It assumes anonymous visitors receive the same page. Cache contexts do not make Internal Page Cache safe for per-session anonymous personalization. If anonymous visitors need different output, evaluate whether to disable this module for that use case and use Dynamic Page Cache with correct metadata.

### 2. Internal Dynamic Page Cache This caches a page minus its personalized parts. Placeholders let Drupal fill those parts in for each request, while reusing the rest. It works for both anonymous and authenticated users. Correct cache contexts and cacheability metadata still matter: placeholders are not permission checks.

### 3. Render Cache Every render array in Drupal can declare its own cache metadata: cache keys, contexts, tags, and max-age. The render cache stores the output of individual blocks, fields, and views rows so they can be reassembled cheaply.

### 4. Cache Backends (Database, Redis, Memcached) A backend is where cache entries are stored, not another kind of page output. Drupal commonly uses the database; Redis or Memcached integrations offer alternatives. Changing storage does not repair missing tags or contexts. Profile your database load and response times before deciding whether another backend is worthwhile.

Cache Tags, Contexts, and Max-Age

Think of a cached article list as a saved answer. You need to know when that answer changes, who can reuse it, and when it expires. Drupal combines data dependencies, request variations and time limits through these three metadata types.

- **Cache tags** describe what the cached item depends on, for example `node:42` or `user_list`. When the underlying data changes, Drupal invalidates everything tagged with it. - **Cache contexts** describe what the cached item varies by, for example `url.path`, `user.roles`, or `cookies:my_cookie`. Different context values produce different cache entries. - **Max-age** is the maximum number of seconds the item can be cached. A value of 0 means "do not cache", and `Cache::PERMANENT` means "cache until invalidated by tag".

Getting these three right is 90% of the job. Most production caching bugs I have debugged came down to a missing cache tag on a custom block or a forgotten cache context on a personalized element.

Example: A Stale Featured-Articles Block

Suppose a custom block lists the five newest published articles. It needs a list tag so adding or removing an article invalidates the results, plus dependencies on the loaded entities so editing their fields invalidates the output. An entity query alone does not automatically attach those dependencies to your render array.

The example below uses CacheableMetadata to merge entity tags and access-result metadata, then applies that metadata to the block. In a real custom module, inject the entity storage and current-user services rather than using static service access.

public function build() {
  $nids = \Drupal::entityQuery('node')
    ->condition('type', 'article')
    ->condition('status', 1)
    ->sort('created', 'DESC')
    ->range(0, 5)
    ->accessCheck(TRUE)
    ->execute();

  $nodes = Node::loadMultiple($nids);

  $build = [
    '#theme' => 'featured_articles',
    '#nodes' => $nodes,
    '#cache' => [
      'tags' => ['node_list:article'],
      'contexts' => ['user.permissions', 'user.node_grants:view'],
      'max-age' => Cache::PERMANENT,
    ],
  ];

  $cacheability = CacheableMetadata::createFromRenderArray($build);
  foreach ($nodes as $node) {
    $cacheability->addCacheableDependency($node);
    $access = $node->access('view', NULL, TRUE);
    $cacheability->addCacheableDependency($access);
    if (!$access->isAllowed()) {
      unset($build['#nodes'][$node->id()]);
    }
  }
  $cacheability->applyTo($build);
  return $build;
}

Import Drupal\node\Entity\Node, Drupal\Core\Cache\Cache and Drupal\Core\Cache\CacheableMetadata for this example. The node_list:article tag tracks list membership; individual node tags track changes to already-listed articles. Access checks prevent rendering inaccessible nodes, while their cache metadata controls safe reuse. Other filters or custom access rules may require additional dependencies.

How to Debug Drupal Cache Issues

When the cache misbehaves, I work through this checklist in order. It catches the vast majority of issues before I have to attach a debugger.

1. **Reproduce the problem twice.** Request the same URL as an anonymous visitor, then as the relevant logged-in user. Inspect X-Drupal-Cache and X-Drupal-Dynamic-Cache when present; their availability and values depend on the enabled modules and response. A MISS alone does not prove a metadata bug. 2. **Enable cacheability debugging locally.** Set the http.response.debug_cacheability_headers container parameter to true in a development services file, ensure that file is loaded, then rebuild the container with drush cr. Inspect X-Drupal-Cache-Tags, X-Drupal-Cache-Contexts and X-Drupal-Cache-Max-Age. Do not assume these debug headers are enabled in production. 3. **Test an actual edit.** Warm the page cache, edit a listed article, then request the page again. Check that entity and list dependencies invalidate the relevant output. 4. **Audit custom render arrays.** Look for missing entity dependencies or contexts on custom blocks and preprocess code. 5. **Check outer caches separately.** If Drupal serves fresh output but the browser still sees old content, inspect browser, reverse proxy and CDN caching before changing Drupal metadata.

# Development services file, loaded by your local settings.
parameters:
  http.response.debug_cacheability_headers: true
# Rebuild after changing container configuration.
drush cr

# Compare response headers on two requests.
curl -sD - -o /dev/null https://your-site.example/articles
curl -sD - -o /dev/null https://your-site.example/articles

Internal Cache vs Reverse Proxy: When to Use What

A reverse proxy such as Varnish or a CDN can serve suitable responses before a request reaches PHP. It complements Drupal's caches, but needs explicit rules for cookies, authenticated responses and invalidation. Tag-based purging depends on the chosen integration and configuration; Drupal does not guarantee instant global purges.

BigPipe streams personalized placeholders to the browser. It is not itself a CDN purge mechanism. Test both freshness after edits and isolation between users before enabling shared caching.

Start with correct Drupal metadata, then measure where requests spend their time. Add Redis, Varnish or a CDN only when your workload and operational needs justify them. A fixed traffic threshold is not a substitute for profiling.

Common Mistakes Drupal Developers Make

- **Clearing everything instead of declaring dependencies.** A rebuild may be needed after configuration or deployment changes, but repeated full clears are not a fix for missing cache tags. - **Setting max-age to 0 without understanding propagation.** Uncacheable child metadata can affect parent output; placeholders can keep dynamic parts separate. Diagnose the dependency before disabling caching. - **Treating caching as authorization.** Use explicit access checks and carry access-result metadata into rendered output. - **Caching a request timestamp forever.** Use a deliberate finite max-age or a dynamic placeholder for time-dependent output. Do not invent a universal time cache context. - **Assuming entity queries attach output metadata.** Merge dependencies from entities, lists and access results into the render array.

Manual vs Automated Cache Invalidation

There are two schools of thought on cache management in Drupal. Manual invalidation, where developers explicitly clear caches after specific operations, gives precise control but is fragile. Automated invalidation through cache tags is the Drupal way: declare your dependencies, and the system handles the rest.

In practice, I aim for 95% automated and 5% manual. The manual escape hatch matters for edge cases like third-party API data refreshes or scheduled content publication, but everything else should ride on cache tags.

Quick Checklist for Drupal Cache Health

- Are entity and list dependencies attached to custom output? - Are contexts set for permissions, URL or other relevant variations? - Is max-age permanent or a deliberate time limit? - Does anonymous personalization avoid unsafe Internal Page Cache reuse? - In development, do debug headers show the expected metadata? - Do content edits refresh both Drupal output and any outer cache? - Do different users receive only content they can access?

Key Takeaways

Drupal caching is about safe reuse, not only speed. Internal Page Cache serves shared anonymous pages; Dynamic Page Cache leaves personalized parts dynamic; Render Cache reuses smaller pieces. Tags control data dependencies, contexts control variations, and max-age controls time limits. Debug the layer serving stale output, test real edits and user access, and measure performance rather than relying on universal latency promises.

Official Drupal Documentation

Cache tags: https://www.drupal.org/docs/develop/drupal-apis/cache-api/cache-tags

Cache contexts: https://www.drupal.org/docs/develop/drupal-apis/cache-api/cache-contexts

Cacheability and debug headers: https://www.drupal.org/docs/8/api/responses/cacheableresponseinterface

Internal Page Cache: https://www.drupal.org/docs/administering-a-drupal-site/internal-page-cache

Dynamic Page Cache: https://www.drupal.org/docs/8/core/modules/dynamic-page-cache/overview

Clearing and rebuilding caches: https://www.drupal.org/docs/user_guide/en/prevent-cache-clear.html

Final Thoughts

Drupal's cache system has a steep learning curve, but once you understand the four layers and the metadata model, it becomes one of the CMS's biggest strengths. Most Drupal developers I know learned this the hard way, through stale homepages and angry editors. You do not have to.

Have you dealt with a tricky Drupal cache bug? I would love to hear about it. Find me on [LinkedIn](https://www.linkedin.com/in/ru5iru) or [X](https://x.com/ru5iru).