Static Caching on Craft Cloud

All of Craft’s default caching features work as you would expect on Craft Cloud.

To supplement this, we provide a static caching system that automatically detects (and purges!) cacheable HTML responses. These static page caches are created and invalidated using the same mechanism as Craft’s own template caches—so any time an element is updated, Cloud knows exactly which pages to purge.

By default, only 200-level GET responses are cached; redirection and errors (300-level and higher) are not cached, unless you explicitly opt in.

Static Cache vs. Template Caches #

Cloud’s static cache operates on full pages, which means that either the entire page is cached, or the entire page is served by your application. Whether you use additional caching strategies in your templates or back-end is up to you!

Craft’s built-in {% cache %} tag can be combined with static caching—but because the caches are invalidated at the same time, they may be redundant. If you have a highly-dynamic front-end that isn’t possible to cache statically, the normal template cache system is still a great option for caching parts of a page.

Most Craft features that rely on dynamic rendering are already set up to bypass the cache, including the entire control panel, live preview, any front-end pages that use the session or cookies. Asynchronous CSRF can be enabled in Craft to make most front-end forms cachable.

Controlling the Cache #

The request’s entire URL (including query parameters) is used when determining whether to serve a page from the cache.

In Craft versions 4.10 and 5.2, the expires Twig tag was introduced to simplify setting cache headers. Examples are provided below for this method as well as precise control of individual headers via the header tag.

Opting Out #

You can explicitly flag a template or response as being ineligible for full-page caching, by setting the Cache-Control header:

{# Set “no-cache” headers by omitting a duration: #}
{% expires %}

{# If the default behavior doesn’t suit your needs, you can set the headers individually: #}
{% header 'Cache-Control: no-store' %}
{# …or, to be equivalent with the PHP function below… #}
{% header 'Cache-Control: no-cache, no-store, must-revalidate' %}

In PHP, use the Response component available from any controller:

public function actionSaveWidget(): Response
{
    // ...

    $this->response->setNoCacheHeaders();
}

This method also sets Expires and Pragma headers. When using the expires tag without any arguments, it ultimately calls the same function:

{% do craft.app.response.setNoCacheHeaders() %}

Force Caching #

If requests are missing the static cache (indicated by a Cf-Cache-Status: MISS response header in your browser’s Network tab) despite meeting the criteria above, and you know that a page should be cacheable, you can explicitly send cache headers:

{% expires in 30 minutes %}

{# ...or prior to Craft 4.10 and 5.2... #}

{% set halfHour = 60 * 30 %}
{% do craft.app.response.setCacheHeaders(halfHour) %}

Craft always sends the appropriate cache invalidation tags so that the page can be purged, later.

Manually caching a page in this way can leak user-specific information. This is only appropriate for use when you are absolutely sure that a page includes no personal details or customizations!

Duration #

The Cloud extension uses the same source of information as Craft when determining how long to statically cache a page (if it is cachable at all). This means that pages using elements with an Expiry Date sooner than the default cacheDuration setting will only be cached as long as all the underlying content ought to be visible. As with Craft’s template caches system, there is not currently a mechanism in place to invalidate caches that would contain elements with a future Post Date.

If have manually set cache headers at some point in the request, Craft will not overwrite

Custom Tags #

Send the special Cache-Tag header to tag responses that you want to be able to purge later, with a known key:

{% do craft.app.response.headers.add('Cache-Tag', 'my-custom-tag') %}

Previously, we used the Twig {% header %} tag to set the Cache-Control header. This is fine in situations where the template can dictate the final value of a header—but tagging pages for Cloudflare’s cache must be done additively with the response’s header collection, without replacing ones that may have been set earlier in the request.

From a controller, you access headers via the same response instance:

$headers = $this->response->getHeaders();
$headers->add('Cache-Tag', 'custom-cache-tag');

Keep in mind that custom tags can only be purged manually, or via a subsequent HTTP response with a Cache-Tag-Purge header. This means that a custom tag sent from a template cannot be purged by the same template, as subsequent requests will be served from the cache and not trigger evaluation. The most reliable way to purge a tag is by sending the appropriate header when a control panel user changes the underlying data.

The format for Craft’s built-in element cache tags is not part of its public API, so we do not recommend attempting to reconstruct them for the purposes of selectively purging caches.

Cache-Tag headers are stripped from the HTTP response at our edge nodes and not forwarded to users.

Manual Purging #

If you would like to clear your environment’s entire static page cache, visit the Utilities screen, check Cloud Static Caches, then Clear Caches.

To clear a specific cache tag, use the CLI via the environment’s Commands screen:

php craft cloud/static-cache/purge-tags tag-one tag-two

You can also directly purge URLs by one or more prefixes:

php craft cloud/static-cache/purge-prefixes mydomain.com/vendors mydomain.com/listings

CSRF #

Use Craft’s asyncCsrfInputs setting to make CSRF inputs generated with the csrfInput() Twig function compatible with the static cache. Instead of outputting the input element and token directly (therefore opening a session), a placeholder is rendered and replaced after the browser/client loads the page and fetches a CSRF token via Ajax.

You can also opt in to asynchronous CSRF inputs on a case-by-case basis:

<form method="post">
  {{ csrfInput({ async: true }) }}

  {# ... #}
</form>

Avoid calling craft.app.request.getCsrfToken() directly, or manually building CSRF inputs. No-cache headers will be sent, regardless of how you generate or access a token!

Flashes #

Any time you access session-dependent information like flashes, Craft sends no-cache headers. Form submissions via POST are never cached and will therefore re-render the page with any contextual validation errors as you would expect—but when the form itself is otherwise cachable (including using asynchronous CSRF tokens), you can guard flash messages with a check for a flag:

{# Access session data only when the `success` query param is set: %}
{% if craft.app.request.getQueryParam('success') %}
  {% set flashes = craft.app.session.getAllFlashes(true) %}

  {% if flashes|length %}
    {% for level, flash in flashes %}
      <p>{{ flash }}</p>
    {% endfor %}
  {% endif %}
{% endif %}

<form method="post">
  {{ actionInput('entries/save-entry') }}
  {{ csrfInput({ async: true }) }}

  {# Append a query parameter to the final redirection: #}
  {{ redirectInput(url(craft.app.request.url, { success: true })) }}

  {# ... #}
</form>

This allows Cloud to cache the form when it is initially requested, while always triggering no-cache headers after a submission by reading flashes from the session.

Applies to Craft CMS 5, Craft CMS 4, and Craft Cloud.