Skip to content

Add Craft Cloud static caching for Craft 6 - #214

Draft
timkelty wants to merge 1 commit into
feature/craft-6-cloud-transformsfrom
cache-control-model
Draft

timkelty wants to merge 1 commit into
feature/craft-6-cloud-transformsfrom
cache-control-model

Conversation

@timkelty

@timkelty timkelty commented Nov 10, 2026 •

Copy link
Copy Markdown
Contributor

Description

Craft Cloud's gateway and CDN cache site pages. Craft has to tell them how long each page may be cached and which content changes should purge it. This adds that, built on Craft 6's existing cache APIs. It's designed from scratch rather than ported from 3.x.

Stacked on #213 for its RequestSigner. GitHub retargets it to 5.x when #213 merges.

Caching pages. The CacheResponse middleware is prepended to Craft's craft group and goes in Laravel's priority list before EncryptCookies. That puts it outside core's SetHeaders and the cookie and session middleware, so it sees the final response. For successful GET and HEAD site responses it:

  • collects tags and expiry with ElementCaches::startCollectingCacheInfo() / stopCollectingCacheInfo(), including the matched element
  • sets CDN-Cache-Control from the response's own Cache-Control: s-maxage, then max-age or Expires. So {% expires %}, ResponseHeaders::setCache() and Symfony's setSharedMaxAge() control the CDN, and the browser header is left alone. Without a lifetime, a page is cached for a year with an hour of stale-while-revalidate. Lifetimes are capped at element expiry. private, no-cache and no-store give no-store, and an explicit CDN-Cache-Control header is left as it is.
  • sets Cache-Tag to the page's Craft tags, each hashed and prefixed with the first 8 characters of the environment ID. If they don't fit within Cloudflare's 1,000 tags or 16 KiB, it adds {env}:overflow.

A response gets no-store when:

  • the request has a session cookie or an Authorization header
  • the response sets a cookie. Lazy site sessions keep pages that only read the session cookie-free.
  • the request used a route token or site token (HAD_TOKEN_KEY, HAD_SITE_TOKEN_KEY). Core only marks preview token responses as no-cache.

Purging. Cloud listens for TagsInvalidated from craftcms/laravel-dependency-aware-cache, which fires for every TagDependency::invalidate(). So the CDN follows Craft's own tag invalidation:

  • element saves
  • direct invalidations, such as the element::Entry::section:N tag invalidated when an entry is moved to another section
  • custom tags from ElementCaches::collectCacheTags()
  • craft:invalidate-tags

Batches with draft or revision tags are skipped, since every autosave invalidates element::{class}::*. Core's internal element-index-query::* and element-search-query::* tags are filtered out as well.

Tags are collected until the request, command or queue job is done, then sent as one queued PurgeStaticCache job. The SQS connection's after_commit sends it after the save commits, failures are retried, and a slow gateway doesn't delay the response. On Lambda, the app terminates before the response is returned. The Caches utility gets a “Craft Cloud static cache” option, which core also exposes as craft:clear-caches:cloud-static-cache. cloud:purge purges everything, or specific tags (--tag) or URL prefixes (--prefix). Both run the job synchronously.

What changed from 3.x, and why

  • Tags: StaticCacheTag and its normalization are gone. There's one function: an 8-character environment prefix plus hex CRC32. It's kept because Cloudflare tags are zone-wide and limited to 16 KiB per response, and a hash collision only causes an extra purge. The response's own Cache-Tag is no longer merged in, since custom tags go through ElementCaches::collectCacheTags().
  • Overflow: {env}:overflow is now purged with every invalidation. In 3.x, pages that dropped tags were only purged by a full purge.
  • Cache-Purge-Tag: dropped. That header only works for web requests, so 3.x needed both it and the API. This uses only the gateway API.
  • URI purges and fetchUrls: dropped. A page is tagged with its matched element, and saving or deleting that element invalidates element::{id}, so tag purges already cover it. fetchUrls only warmed the saved element's own URL. The 3.x Dev-Mode header is dropped too.
  • Commands: five cloud/static-cache/purge-* commands become one cloud:purge, plus the Caches utility option.
  • Config: there's no config('cloud.*'). Durations are no longer configurable, since sites set them per response with Craft's own APIs. The package reads the CRAFT_CLOUD_ENVIRONMENT_ID, CRAFT_CLOUD_SIGNING_KEY and CRAFT_CLOUD_GATEWAY_BASE_URL env vars Cloud already sets, like Register Craft Cloud storage as Laravel disks #212 and Add the Craft Cloud asset transform driver #213.
  • GraphQL workaround: not carried over, as Fix invalidation of cached GraphQL responses cms#19508 is in 6.x.

Contract questions for infra. None of these needs a change:

  • If the gateway already purges {env}:overflow with every tag purge, the line that adds it here can go.
  • Purges now run as Laravel queue jobs. Until the bref laravel-bridge SQS handler replaces JobSqsHandler (an open plan item), queued purges won't run on Cloud. Only cloud:purge and the Caches utility work until then.
  • If anything outside Craft runs the 3.x cloud/static-cache/* commands, it needs to run cloud:purge instead.

Dependencies. None. This doesn't change composer.json or the lock. #213 already requires craftcms/cms ^6.0.0-beta.1 and craftcms/http-message-signatures. The lock there has cms at 6.x-dev 05ce9b8, the beta.1 commit, because Packagist hasn't indexed craftcms/cms-assets 6.0.0-beta.1 yet, and cms beta.1 requires it at self.version. CloudServiceProvider gets a single line.

Related issues

@timkelty timkelty changed the title Integrate Cloud response caching with CMS cache-control policy Add Craft Cloud static caching for Craft 6 Nov 10, 2026
@timkelty
timkelty force-pushed the cache-control-model branch from fdf8c71 to c0d2bac Compare November 10, 2026 18:22
@timkelty
timkelty changed the base branch from 5.x to feature/craft-6-cloud-transforms November 10, 2026 18:22
@timkelty
timkelty force-pushed the cache-control-model branch from c0d2bac to 5f931b2 Compare November 10, 2026 18:24
Tag pages with their Craft cache tags and derive CDN-Cache-Control from
the final response headers. Purge on every TagDependency invalidation,
except drafts and revisions, with one queued PurgeStaticCache job per
request, command or queue job, signed with #213's RequestSigner. Add a
Clear Caches option and a single cloud:purge command.

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-6551416a-ce7c-4e90-adac-8c2edc72248c
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant