Caching — the Velocity Engine
The Velocity Engine is nginx’s FastCGI cache, configured per site, plus a WordPress plugin that tells the panel exactly which pages to invalidate. There is no third-party cache plugin and no purge module.
The important property: a cache hit never runs PHP. nginx serves it from disk. That is why cached throughput is roughly 1,500× the uncached rate on the same machine.
What is cached
Section titled “What is cached”Anonymous GET and HEAD requests that return 200 or 301. That is it.
What is never cached
Section titled “What is never cached”A request bypasses the cache entirely if any of these is true:
| Condition | Why |
|---|---|
the method is POST | it changes something |
| there is a query string | ?add-to-cart=, ?s=search, tracking parameters |
| a login cookie is present | wordpress_logged_in_, wp-postpass_, comment_author_ |
| a cart cookie is present | woocommerce_cart_hash, woocommerce_items_in_cart, wp_woocommerce_session_ |
| the path is dynamic | /wp-admin, /wp-login.php, /wp-cron.php, /xmlrpc.php, /wp-json/, /feed, /cart, /checkout, /my-account, /addons, /wc-api |
Check any URL:
curl -sI https://example.com/ | grep -i x-slipstream-cache # HIT / MISScurl -sI https://example.com/cart/ | grep -i x-slipstream-cache # BYPASSHIT served from cache · MISS generated and stored · BYPASS never eligible ·
STALE served stale while revalidating · UPDATING a refresh is in flight.
Pre-compressed storage
Section titled “Pre-compressed storage”Most panels store cached HTML uncompressed and gzip it on every request. That makes gzip the throughput ceiling — we measured 944 req/s doing it that way versus 2,884 req/s without compression at all.
Slipstream has PHP compress its own output (zlib.output_compression), so nginx caches the
already-compressed body and serves hits verbatim. The cache key includes a normalised
Accept-Encoding bucket so a gzip variant and an identity variant are stored separately and never
served to the wrong client. That single change took cached WooCommerce throughput from 944 to
8,664 req/s on a 2-core box.
Coalescing, stale-while-revalidate, stale-if-error
Section titled “Coalescing, stale-while-revalidate, stale-if-error”Three behaviours that matter when a popular page expires:
- Request coalescing (
fastcgi_cache_lock) — if 500 visitors hit an expired page at once, one request regenerates it and the rest wait briefly for that result. Without this you get a stampede: 500 simultaneous PHP renders on a box that can serve six. - Stale-while-revalidate — an expired page is served immediately from the stale copy while the refresh happens in the background. Visitors never wait for a regeneration.
- Stale-if-error — if PHP returns 500/503, times out or the upstream is down, the stale copy is served instead of an error page. A crashed PHP pool degrades to slightly-old content rather than an outage.
Invalidation
Section titled “Invalidation”The Slipstream connector — a WordPress mu-plugin installed automatically — watches for content changes and asks the panel to purge exactly the affected URLs, not the whole site:
Publishing or updating a post purges that post’s permalink, the home page, its post-type archive, the feed, each of its categories and tags, the author archive and the year/month archives. A new comment purges only that post. Switching theme, activating or deactivating a plugin, editing a menu or saving the customiser purges everything, because those can change any page.
Purge manually:
slipctl purge <site-id> # everythingslipctl purge <site-id> https://example.com/a-page/ # specific URLsObject cache
Section titled “Object cache”Separate from the page cache. The page cache stores finished HTML; the object cache stores the database query results WordPress builds that HTML from, and is what speeds up logged-in traffic and cache misses.
APCu is the default, and on a single server it beats Redis: it lives in the PHP process, so there is no socket round trip and no serialisation. Measured on a WooCommerce store, forcing Redis made things slower — 4.13 req/s versus 4.56 with APCu. Redis remains available for when you genuinely have more than one application server.
wp-cli and PHP-FPM have separate APCu segments, and no amount of flushing from one reaches the other. An FPM reload does not clear it either: the master process survives and keeps its shared memory. Only a restart does.
The drop-in works around that instead of living with it. Every key carries an epoch read from
shared/.slipstream-cache-epoch, a file both processes can see. A flush writes a new epoch, so
the next request in every process computes different keys and the old ones are unreachable.
wp cache flush from the command line therefore takes effect on the web tier immediately, and so
does anything else that flushes: activating a theme, changing an option, a restore.
This used to be a real trap rather than a footnote. wp-cli runs with apc.enable_cli=0, so the
segment is never initialised in that process, and the drop-in’s guard against that case did
nothing: PHP hoists top-level function and class declarations, so a bare return above them
never stopped them being declared. On a wp-cli invocation wp_cache_set(), wp_cache_add(),
wp_cache_delete() and wp_cache_incr() all returned false, wp_cache_add() could not take the
lock WordPress uses it for, and wp cache flush died with “APC must be enabled to use
APCUIterator”. On a server with no APCu extension at all it was worse: the first cache write
fatalled the site. The drop-in now detects the case and falls back to a request-scoped cache,
which is what WordPress does natively.
Cache warming
Section titled “Cache warming”POST /api/sites/{id}/warmCrawls the site’s sitemap and requests each URL so the cache is populated before visitors arrive. Worth doing after a deployment or a full purge.
Tuning
Section titled “Tuning”Per site: cache_enabled, cache_ttl_sec (0 = the profile default), and the profile itself
(Balanced 10 min / Commerce 5 min / Maximum 24 h). See Concepts.
Cache files live in /var/cache/slipstream/<domain>/, with an in-memory key zone sized per site
and a disk cap. Entries are evicted least-recently-used when the cap is reached, so the cache
cannot fill the disk.
When caching is not the answer
Section titled “When caching is not the answer”The page cache does nothing for logged-in users, cart and checkout, or an API returning per-user
JSON. If those are your bottleneck, the levers are the object cache, the application’s own query
count, and PHP’s own speed — not the page cache. Benchmarks has the measured
uncacheable numbers, including the 24% cost of the open_basedir jail we keep on purpose.