Guide 12 · Developer docs

Performance, caching, deployment and go-live

How Core dna caches pages, what makes a render fast, how code reaches a site and what stays behind, the go-live checklist, and running a site once it is live. For developers, leads and site operators who already build pages in a theme repository.

L2–L3
Unit 01 · The caching model

What makes a response uncacheable

The rules that decide whether a render is stored, the routes that refuse it themselves, the accidental ways a template breaks caching in either direction, query parameters, and the order to check things in when a page will not cache.

Who this is for. Theme developers and reviewers. Afterwards you can say why a given page is or is not stored, spot the template habits that break caching, and run the diagnostic when a page answers miss every time.

A page can fail the cache in two directions: it is never stored, so every visitor waits for a render, or it is stored when it should not be, so visitors see the wrong thing. The second is more harmful and harder to notice.

The rules a render must pass

After a page renders, the platform stores it only if all of these hold:

  1. The environment is not a development environment. Nothing overrides this one.
  2. No platform route, and no visitor language cookie, switched caching off for this request (below). Nothing overrides this one either.
  3. The template did not call preventCaching(), unless it also called enforceCaching(), which skips this rule and every rule after it (the other controls).
  4. The request was a GET or a HEAD.
  5. The response is a 200. Redirects, 404s and errors are never stored.
  6. No visitor was signed in when it rendered (authenticated requests).
  7. The request did not ask for JSON with Accept: application/json.
  8. The path and query string match none of the patterns in Cache settings › Response cache exclusions.

Deliberate causes

  • Your preventCaching(), for a response that genuinely differs per visitor (preventCaching() and fragments).
  • The exclusions setting. One regular expression per line, matched against the path and query string. The platform wraps each line in / delimiters, so escape any slash in the pattern: ^\/events\/calendar, not ^/events/calendar. A pattern stops new copies being stored; it does not remove copies already stored, so clear the cache after adding one.
  • Routes that prevent caching themselves: the cart page, the single-page checkout and its payment processing step, the invoice payment return page, the sitemap, e-mail confirmation, passkey prompts, the Documentation module's pages and any revision preview. A search page is not on the list: it is stored once per declared query parameter like any other page, and only a search that failed (the search engine could not answer) is kept out of the cache, so an outage is not stored as "no results" (caching a search page).
  • A visitor's language cookie. When a visitor has chosen a language through the cdna_locale cookie and it differs from the site's language setting, their render is not stored, because the cache key cannot see cookies.

Accidental causes

These come from ordinary template code and produce no error:

  • preventCaching() in a shared partial. A call in the header, the footer or a component used site-wide switches the cache off for every page that includes it. Search the templates for the call and check what includes each file that has one.
  • A template redirect on a common path. $_response->redirect() answers 302 by default, which is never stored, so a page that redirects "just in case" renders every time.
  • A 404 that should be a 200. A not-found answer is rendered for every request: typically a listing whose page number ran past the end, or a custom not-found template doing real work.
  • A cookie set in a template. $_response->setCookie() does not stop the page being stored. The stored copy keeps its content and a short list of headers (content type and length, ETag, the content security policy and the CDN tags), and Set-Cookie is not one of them. Only the visitor whose request rendered the page gets the cookie; every later visitor gets the page without it. Set cookies from a fragment or from JavaScript, never from a cacheable page.
  • Personal output on a public page. A render for a signed-out visitor that prints anything about that visitor, such as a guest cart count read through $_cart, a cookie value or a geolocated greeting, is stored and served to everyone (the cart count is wrong or stale).

Query parameters

The page cache key is the hostname and the path, plus only the query parameters the response declared. A template declares a parameter in one of two ways: by reading it through $_request->getParameter('name'), or by naming it with $_response->addCacheVaryParameter('name') (or several with setCacheVaryParameters([...])). Nothing else declares one: not $_request->getParameters(), not Smarty's $smarty.get, and not a controller that read the parameter for you. Two failures follow:

  • An undeclared parameter is invisible. If /events?month=6 is rendered first and the template read month some other way, that render is stored under /events and served for every month, and for /events itself. Module listings and search pages read their parameters in the controller, so their templates must declare the parameters they depend on, the page number included (caching a search page).
  • A declared parameter you did not need splits the cache. Every distinct value is a separate stored copy, so a template that reads utm_source or a click id stores one copy per campaign link and seldom gets a hit. Read tracking parameters in JavaScript, not in the template.

Declare a parameter by reading it, even when you only branch on it:

Template code
{$month = $_request->getParameter('month')}
{if $month}
  {include 'events/includes/month.html' month=$month}
{else}
  {include 'events/includes/upcoming.html'}
{/if}
{% set month = _request.getParameter('month') %}
{% if month %}
  {% include 'events/includes/month.html' with {month: month} %}
{% else %}
  {% include 'events/includes/upcoming.html' %}
{% endif %}

Two details of the key. Parameter names are compared in lower case and in any order, so ?b=2&a=1 and ?A=1&b=2 share a copy; values are compared as sent. And PHP turns a dot in a parameter name into an underscore before the template sees it, so ?tags.name=x is declared as getParameter('tags_name').

Finding out why a page is not cached

Work signed out, on production or a staging environment that runs in production mode (development never stores a page). Request the page twice and read the headers:

Shell
for i in 1 2; do
  curl -s -o /dev/null -D - "https://www.example.com/events" | grep -iE '^HTTP|x-cache-status|cache-control'
done
  1. The status is not 200. Fix the redirect or the error first; nothing else matters until it is a 200.
  2. miss, no-store both times: one of the rules refused it. Search the templates the page uses, every include and the base template included, for preventCaching; check the exclusions setting, and that its patterns escape their slashes; check whether the route is one that prevents caching itself.
  3. miss, store both times: it is stored, and the second request is not finding it. Usually the URLs differ in a declared parameter, the hostname differs (www against the bare domain), or the copy was cleared in between by smart invalidation on a busy site.
  4. No X-Cache-Status on the render, and Cache-Control: public: CDN caching for pages is on. The page was stored; the CDN's own header says whether the CDN served the next one (reading the headers).
  5. hit, but the content is wrong: it is cached correctly and should not be. Look for an undeclared parameter or personal output, above.
Go deeper

Was this page helpful?

Your answer goes to the docs team.