Skip to main content
Drupal is a registered trademark of Dries Buytaert
Release: Drupal 12.0.0-beta1 — First beta version released for Drupal core (12.0.0-beta1). Release: Views Bulk Operations (VBO) 4.4.9 — Minor update available for module views_bulk_operations (4.4.9). Release: External Authentication 2.0.15 — Minor update available for module externalauth (2.0.15). Release: External Authentication 2.0.14 — Minor update available for module externalauth (2.0.14). Release: External Authentication 2.1.0-beta2 — New beta version released for module externalauth (2.1.0-beta2). Release: AI Image Alt Text 1.0.3 — Minor update available for module ai_image_alt_text (1.0.3). Release: Search API attachments 10.0.12 — Minor update available for module search_api_attachments (10.0.12). Release: Field Validation 3.0.0 — Major update available for module field_validation (3.0.0). Module Revived: Icon Select 3.0.2 — Module icon_select updated after 24 months of inactivity (3.0.2). Usage Milestone: MathJax: LaTeX for Drupal — Module mathjax crossed 1,000 active installs.

Really Simple Embettered Page Cache

No security coverage Drupal 10–11
View on drupal.org

Drupal's page cache is great while it's populated, but once a page is invalidated (for example when its content changes, or after a cache clear), the next request which required the page to be rebuilt is blocked until the page has been built.

On a busy site, or a site which is being crawled aggressively, this can cause a traffic stampede to build multiple pages, which in turn can overwhelm most web servers.

This module will continue to serve cached pages after they become "stale" (that is, after the content they show has changed, or after the cache has been cleared), but in the background will rebuild the page, without blocking requests.

Background page refreshes happen immediately at the end of the current request, up to a maximum of two (configurable in admin > config > development > performance) concurrent build sessions; if more than this number of stale pages need to be built at the same time, further requests are deferred to a queue which is processed by cron.

The problem

Drupal's internal page cache stores each anonymous page as permanent, together with its cache tags. When a tag the page depends on is invalidated, or the cache is cleared, the next visitor has to wait while Drupal builds the whole page again.

Invalidation is frequent on any site with listings. Saving a single node invalidates node_list, which affects every page that lists content, and every deployment that runs drush cr empties the page cache entirely.

What this module does

When a visitor asks for a page that is:

  • cached and valid: they get the cached page (X-Drupal-Cache: HIT), exactly as in core.
  • cached but invalidated: they get the stale copy immediately (X-Drupal-Cache: STALE). The page is then rebuilt after the response has been sent, or queued for cron.
  • not cached at all: the page is built on the spot (MISS), exactly as in core.

At most two pages are rebuilt at once, whether on request or by cron. A stale page that finds both slots busy is added to a queue and rebuilt by cron. A queued page that is valid again by the time cron reaches it, or has been purged since, is skipped.

A full cache rebuild (drush cr) no longer deletes cached pages. It marks them stale, so the first visit to each page after a deployment is served instantly and the page is rebuilt behind it.

Rarely visited pages benefit most. A page can be served stale for up to a year by default, so it stays fast even if nobody has requested it for a long time.

Cache-Control on stale responses

A stale response carries a short Cache-Control: public, max-age=N. N is the smallest of the stale max-age option (60 seconds by default), the max-age the page was stored with, and the site's page cache maximum age (the "Browser and proxy cache maximum age" setting on the Performance page). That setting does not decide when a page becomes stale, only how long a browser or CDN may keep it.

Without this, a CDN or reverse proxy in front of the site would keep the stale copy for the full page maximum age. Fresh and rebuilt pages keep the header they always had. A site whose page maximum age is 0, and any page sent as private or no-cache, is left exactly as it was.

Requirements

  • Drupal 10 or 11, with the core Internal Page Cache (page_cache) module enabled.
  • PHP 8.1 or later.
  • No contributed modules are required. It works with whatever backend the page cache uses (database, Redis and so on) and needs nothing added to settings.php.

Settings

All options are optional. They are on the Performance page (admin > config > development > performance), in a "Stale-while-revalidate page cache" section. Any option can also be fixed in settings.php with the key shown below. A value in settings.php always wins, and the Performance page then shows that option read-only, with a note saying why.

  • $settings['embettered_page_cache_enabled'] (default TRUE): a kill switch. FALSE behaves exactly like core, including drush cr deleting pages.
  • $settings['embettered_page_cache_stale_max_age'] (default 60): seconds a stale response may be cached downstream.
  • $settings['embettered_page_cache_max_stale_age'] (default 31536000, one year): the oldest a page may be and still be served stale. It is also the lifetime given to every stored page, so invalidated pages are eventually reclaimed. Older pages are built on the spot.
  • $settings['embettered_page_cache_rebuild_slots'] (default 2): how many pages are rebuilt at once, on request or by cron.
  • $settings['embettered_page_cache_lock_timeout'] (default 120): crash-safety timeout on the rebuild locks. Keep it at or above your PHP-FPM request_terminate_timeout.
  • $settings['embettered_page_cache_queue_dedupe_window'] (default 900): seconds before a queued page may be queued again. About one cron interval.
  • $settings['embettered_page_cache_failure_backoff'] (default 60): seconds to leave a page alone after a rebuild could not be stored.

Things to know

  • drush cr no longer clears cached pages. To delete them for real, for example to see a configuration change straight away, run drush php:eval "\Drupal::cache('page')->purge();".
  • Installing the module empties the page cache once. Pages stored before it existed lack the tag that drush cr now invalidates, so they would otherwise survive a deployment with the old release's markup.
  • A page that has never been cached is still built on the spot, because there is nothing to serve.
  • Cached 403 and 404 responses are not served stale. A page that was missing is far more likely to exist now than to still be missing, so it is rebuilt straight away.
  • A page that will not cache is not retried on every visit. If a rebuild returns something core will not store, such as a server error or an exception, the page is left alone for a short back-off period. If the page now comes back gone or forbidden, its stale copy is deleted. A server error keeps the stale copy, because stale is better than an error.
  • A rebuild sees its own request. Code that reads $_SERVER, $_GET or $_COOKIE directly gets values describing the page being rebuilt, with the visitor's cookies and headers removed. Cron rebuilds run as the main request, and the active theme, static caches, entity memory cache and page cache kill switch are reset between pages.
  • Old cache entries can be unreadable after a release. If a cached page no longer unserialises, it is treated as not cached and overwritten.

Known limitations

  • Services first used during a rebuild that follows a stale response are not destructed, because core has already finished its own shutdown work by then. They are persisted by the next full request.
  • Code that calls exit() during a request kills the rebuild, and any locks it held lapse after the lock timeout. This only matters for a queued URL that such code would redirect, which a page that was cached in the first place normally is not.
  • Rebuilt pages use the triggering visitor's timezone if a module changes it per visitor (for example by GeoIP), the same as core's first-visitor-builds-the-page behaviour. Cron rebuilds use the site default.
  • The persistent lock backends share one owner ID, so a rebuild that outlives the lock timeout can release another process's lock.

Monitoring

  • X-Drupal-Cache: STALE on a response means a stale copy was served.
  • The really_simple_embettered_page_cache log channel records rebuilds that could not be stored (warning), exceptions (error) and queueing (debug).
  • drush queue:list shows pages waiting for cron.

How it works

  • The page cache middleware and the cache.page bin are altered in a service provider, so nothing needs configuring and the module works on any cache backend.
  • Rebuilds run in a PHP shutdown function, after the response has been sent to the visitor. Drupal's own terminate() is not used, because it only runs after the kernel has been prepared, which a page cache answer never reaches.
  • Every stored page carries one extra cache tag, and deleting the whole page cache invalidates that tag instead of deleting rows. The backend's own invalidateAll() is deprecated from Drupal 11.2, so it is not used.
  • Concurrency is limited with Drupal's Lock API: a counting semaphore built from named locks, a per-page rebuild lock, and a per-page queued lock that is deliberately never released, so its timeout is what allows the page to be queued again.

Tests

The module ships with unit and kernel tests. The kernel tests need SIMPLETEST_DB and check the service alterations against a compiled container and a real database cache bin, including that a cache flush leaves pages stale rather than deleting them.

Depends on

Dependencies of the latest stable release

  • page_cache Drupal core

Required by

Tracked projects that depend on this one

No tracked projects depend on this one yet.

Activity

Tracked releases
1
Tracked since
Oct 2026
Latest release
6 hours ago
Releases (12 mo)
1 ▲ from 0
Maintenance
Active

Releases

Version Type Core Release date
1.0.0 Stable 10–11 Oct 1, 2026