Skip to the content
Documentation
Help Start
Reading in:
  • German: 9,059 of 9,059 pages
About the help in other languages
All help pages

Velocity response cache

This page is for administrators who serve public pages with Velocity and want them answered in a fraction of a millisecond. The response cache keeps rendered public pages and answers repeat requests inside the server…

Download as PDF

Address: https://doc.exponential.earth/exphelp/viewer/features/6.0/velocity-response-cache.md

On this page

Velocity response cache

This page is for administrators who serve public pages with Velocity and want them answered in a fraction of a millisecond. The response cache keeps rendered public pages and answers repeat requests inside the server process, before a worker is chosen, so a hit costs no worker at all. It behaves like a small reverse proxy: conditional requests (304), ETag, Age, stale-while-revalidate, negative caching, compressed storage, and invalidation by a single touched file. Applies to Exponential Velocity 0.0.4.x.

A cached public page is served in about 0.2 ms of CPU (measured on a 12-core host: a front page over TLS with gzip, about 2,950 pages a second from one server, 11,985 from four servers on one port). Rendering the same page takes hundreds of milliseconds. A busy page never sends every visitor to PHP at the same moment, because one request renews an expired page while the others are answered from the old copy.

Turn it on

The cache is off until a setting says enabled: true. Since release 0.0.4.39 no cache setting means no cache. (Before it, the built-in default was on, and disabling the cache module did not stop it.)

  1. In a site file or a module such as mods-available/cache.conf:

    { "Q": { "web": { "cache": {
      "enabled": true,
      "dir": "/var/cache/qbix/example.com"
    } } } }

    With a configuration directory, qbixctl enmod cache turns it on at the next restart and qbixctl dismod cache turns it off. A module that only sets dir or defaultTtl does not turn it on. The site file (--config) is read last, so enabled: false there wins over a module; a setting saved in the control panel wins over both.

  2. Let the application say how long a page may live:

    <?php
    Q_Response::header('Cache-Control: public, max-age=300');
    echo renderFeed();
  3. Restart or reload the server so it picks up the setting.

  4. Check: request the same public page twice.

    curl -sI https://example.com/ | grep -iE 'x-cache|age'

    The second answer carries X-Cache: HIT and an Age header.

What is kept

A response is stored only when all of these hold:

  • the request is a GET for a path that is not the server's own (/Q/... and /.well-known/... are never cached);
  • the status is 200, or 404/410 when negativeTtl is set;
  • the request carries no Authorization header and none of the skip.cookies;
  • the response sets no cookie;
  • Cache-Control says neither no-store nor private.

The key is host, path, query string and the coding the body is stored in (gzip for clients that accept it). Brotli and gzip clients share one entry. A q=0 in Accept-Encoding counts as "not accepted".

A visitor's own Cache-Control: no-cache does not bypass the cache, so pressing reload cannot make the server render every page again. A cache warmer sends the header x-cache-refresh instead.

The lifetime is s-maxage, else max-age, else defaultTtl (default 0, which stores nothing).

Entries live in APCu for small ones when usable, and as one file per page on disk below dir, in a two-level directory.

Invalidate everything after a deploy: the generation marker

A changed template or stylesheet is invisible to a cache that judges pages by what the application says. After a deploy, touch the marker:

touch /var/cache/qbix/example.com/.generation
qbixconsole cache:clear

Only the file's modification time counts. Every entry stored at or before that time is treated as a miss and replaced on its next request. Each process reads the marker at most once a second, so every worker and every other server sharing the file sees it within a second. From Exponential, exp:velocity cache clear does the same, and exp:velocity deploy calls it last (Velocity engines).

Read it: headers and health

HeaderWhen
X-Cache: HITServed from the cache
X-Cache: STALEServed from an expired copy while a new one renders
AgeSeconds since the entry was stored
ETagOn every stored page

/Q/health (full figures need admin access) reports hits, misses, hitRate, stale, hitsFrom.index|memory|apcu|disk, memory.* and apcu.*. Mostly disk with APCu enabled means APCu is not doing its part: check apc.enable_cli.

Settings

All under Q.web.cache.

Configuration keyDefaultMeaning
enabledfalseTurn the cache on
dir<app>/files/cache/reverseWhere entries and the marker live
defaultTtl0Lifetime in seconds for a response that states none
skip.cookies["Q_sid", "PHPSESSID"]Cookie names (matched by prefix) that mark a request as personal
staleWhileRevalidate0Seconds an expired page is still served while one request renders a new copy
revalidateLockSeconds30After this long, a request renewing a stale page is presumed gone
negativeTtl0Seconds to keep 404 and 410 answers
refreshHeaderx-cache-refreshRequest header that renders and stores the page again
generationFile<dir>/.generationThe generation marker
apcu.enabledwhen APCu is usableKeep small entries in APCu as well as on disk (needs apc.enable_cli=1 under the CLI)
apcu.maxSize65536Largest body in bytes kept in APCu
memory.maxEntries0Pages kept in the server's own memory in front of APCu; 0 turns the layer off
memory.maxBytes33554432Most bytes of bodies kept in memory
memory.maxEntrySize65536Largest body kept in memory
minifyHtmlfalseCollapse whitespace in cached HTML
middleOutfalseCompress stored bodies against a shared dictionary (measure first)
fileMode0666 less the umaskMode of entry files
dirMode0755Mode of cache directories
sweep.every300Seconds between sweeps of expired files; 0 turns the sweep off
sweep.budget2000Most files one sweep looks at
sweep.maxAge0Also remove files older than this many seconds
pauseFilenoneWhile this file exists, neither this cache nor the application's cache answers (maintenance window)

Use it from the control panel

The panel's Cache tab (/Q/panel) offers switches for the cache, APCu and the memory layer, the live hit rate, presets for the lifetimes, Clear everything, purge by URL or pattern, warm a page, and a browser of stored pages. Settings saved there live in the panel store and win over the file. Only ten settings are taken from it: enabled, defaultTtl, staleWhileRevalidate, negativeTtl, skip.cookies, apcu.enabled, apcu.maxSize, memory.maxEntries, memory.maxBytes, minifyHtml. The API is under /Q/api/cache.

An application's own cache

The response cache skips requests with a session cookie, because it cannot tell visitors apart. An application that keeps rendered pages per visitor or role can be asked first, in the server process, so a hit never wakes a worker:

{ "Q": { "web": { "appCache": {
    "file":  "/path/to/app/lib/PageCache.php",
    "class": "PageCache",
    "dir":   "/path/to/app/var/page-cache"
} } } }
  • class needs a static fromDir($dir) and a serve(array $request) that returns [status, headers, body] for a hit and null otherwise.
  • The request carries scheme, host, uri, method, cookies, acceptEncoding, ifNoneMatch, and (since 0.0.4.35) headers and port.
  • A request with a session cookie goes to the application's cache only. Any other request is answered by the response cache first and the application's cache on a miss, and that answer is kept in the response cache.
  • Because the response cache then answers first, point Q.web.cache.generationFile at a file every application purge rewrites (Exponential's exp:velocity uses the HTTP cache's state file).

Limits and traps

  • Do not cache personalised pages. Anything with a cookie, Authorization, private or no-store is skipped; keep that true in your templates.
  • Upgrading across 0.0.4.29: cached pages are filed under the coding they are stored in, so the first request for each page after the upgrade renders again.
  • Upgrading to 0.0.4.39: an installation that relied on the old default with no enabled anywhere stops caching; add {"Q":{"web":{"cache":{"enabled":true}}}}.
  • APCu under the CLI needs apc.enable_cli=1; the server says so at start when it is off.
  • The in-memory layer saves about 12 percent CPU per hit for large pages and nothing for small ones; measure on your own site.

Full upgrade steps: Velocity engine upgrade notes.

Become an Exponential Partner

Partners share their information, provide funding, contribute back and develop the products with us, and that is how Exponential grows. A partner website and dashboard are planned for mid-2027: please become a partner today to speed up our growth rate and land new features faster.

Something wrong on this page? Report an error

Documentation license

This documentation is free documentation, licensed under the GNU Free Documentation License 1.3 or later.

Exponential Copyright © 1998-2026 Exponential Foundation and 7x