Nginx FastCGI Cache and Microcaching on a VPS: Configs for Dynamic Sites

Every PHP request on a typical WordPress or Laravel VPS spends 100–300 ms in PHP-FPM even when the rendered page barely changes. Nginx’s fastcgi_cache stores the finished HTML and serves repeat visitors from memory or disk in under 10 ms — a 10–30x drop in time-to-first-byte using one http block and a couple of location directives. Unlike proxy_cache, it is built for upstreams speaking FastCGI, so it applies directly to php-fpm.

The cache zone lives in RAM for its key index and on disk for the payloads, and it competes with your other disk usage. see the full specs on our VPS comparison table and give the cache its own partition if your provider allows it.

How fastcgi_cache Works

fastcgi_cache_path /var/cache/nginx levels=1:2
                   keys_zone=DYNAMIC:100m inactive=60m max_size=2g;
  • keys_zone holds the metadata index in RAM — 100m handles roughly 100,000 cache entries.
  • levels=1:2 spreads files across subdirectories so the filesystem does not choke on one giant directory.
  • inactive=60m evicts entries not accessed for an hour, and max_size caps the disk footprint.

Caching the PHP Location

location ~ \.php$ {
    fastcgi_cache DYNAMIC;
    fastcgi_cache_key "$scheme$request_method$host$request_uri";
    fastcgi_cache_valid 200 301 302 60m;
    fastcgi_cache_use_stale error timeout updating;
    fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    include fastcgi_params;
}

The cache key must include scheme, method, host, and URI so HTTPS and HTTP versions, or two domains on one server, never serve each other’s pages. fastcgi_cache_use_stale serves a stale copy when PHP-FPM is down — the difference between a degraded page and a 502.

Set the key at the http level so every server block shares it, and keep the zone name unique per site when several applications run on one VPS. If you serve both www and apex domains, redirect one to the other first — otherwise the same page is cached twice and the hit ratio drops.

One warning about fastcgi_cache_valid: it only sets TTLs for status codes you name, and any response without a matching rule is not cached. Include 200, 301, and 302 for normal pages, and give 404s a short TTL so a flood of bad-URL requests does not hammer PHP-FPM either.

Microcaching: Cache Short, Cache Often

fastcgi_cache_valid 200 1s;
# or, for a news site: fastcgi_cache_valid 200 5s;

Microcaching sets a TTL of one to five seconds. It is safe for logged-out traffic because the page changes so little in that window, yet it absorbs the thundering herd when a front-page link or a newsletter blast hits: hundreds of concurrent visitors collapse into one PHP-FPM request per second instead of hundreds.

Combine microcaching with fastcgi_cache_lock on to prevent cache stampedes. When the TTL expires and ten requests hit the same URL at once, the lock lets only one reach PHP-FPM while the others wait for it to repopulate the cache — without the lock, all ten would regenerate the page and you would be back to the load you were avoiding.

  • Start at 5s for a blog or news site and 1s for pages with user-specific widgets you cannot easily bypass.
  • Watch the cache hit ratio in logs; if it stays below 80%, your cache key or bypass rules are too aggressive.
  • Reserve 15–20% of RAM for the keys_zone — a swap-thrashing server with a huge cache index is slower than no cache at all.

Bypassing the Cache for Logged-In Users

set $skip_cache 0;
if ($request_method = POST)      { set $skip_cache 1; }
if ($query_string != "")         { set $skip_cache 1; }
if ($request_uri ~* "/wp-admin/|/wp-json/|/cart/|/checkout/") { set $skip_cache 1; }
if ($http_cookie ~* "wordpress_logged_in|woocommerce_items_in_cart") { set $skip_cache 1; }

fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;

Logged-in sessions, shopping carts, and POST requests must never be cached — a cached checkout page is a data leak. These if guards flip $skip_cache and both directives honor it: bypass skips reading the cache, no_cache skips writing it.

The if blocks are evaluated at rewrite time and are safe here because they only set a variable — they do not perform actions like redirects or rewrites, which is where Nginx if gets dangerous. For WooCommerce or other cookie-heavy apps, add your plugin’s cookies to the regex so cart and session pages always hit PHP-FPM.

Purging the Cache on Content Updates

map $request_method $purge_method {
    PURGE 1;
    default 0;
}
location ~ /purge(/.*) {
    allow 127.0.0.1;
    deny all;
    fastcgi_cache_purge DYNAMIC "$scheme$request_method$host$1";
}

With ngx_cache_purge compiled in, a curl -X PURGE http://127.0.0.1/purge/ from a save_post hook or a deploy script invalidates exactly the affected URLs instead of flushing the whole zone.

If you cannot compile the purge module, the fallback is a short TTL: pages self-invalidate within their validity window, so a 60-second TTL means the worst case is a one-minute-old page after an update. Many WordPress setups pair a 60s TTL with a purge plugin and get both freshness and most of the performance win.

Parameters That Matter

ParameterEffectTypical value
keys_zone sizeMetadata memory100m per ~100k URLs
inactiveEviction window60m
fastcgi_cache_validPer-status TTL200 60m; 404 1m
max_sizeDisk cap2g
use_staleServe during upstream failureerror timeout updating

Testing with curl

curl -sI https://example.com/ | grep -i x-cache-status
# MISS on the first hit, HIT on the second — that is the cache working

Add add_header X-Cache-Status $upstream_cache_status; in the location while testing, then remove it in production so the header does not leak to visitors.

FastCGI caching is the single highest-leverage change for PHP sites on a small VPS — it converts a burst of concurrent visitors into one upstream request. Tune the TTL per page type and watch the hit ratio in your access logs. When you size the server for it, compare plans side by side on our comparison table to get enough RAM for both PHP-FPM and the cache zone.

Prefer to tune cache headers instead of hand-editing Nginx configs? Cloudways enables Varnish and Redis caching from its control panel on every managed VPS plan. See Cloudways plans and pricing if you want caching without the config file.

Leave a Reply