Serving Stale Content: Nginx proxy_cache stale-while-revalidate and stale-if-error

Most Nginx caching advice stops at proxy_cache_path and a proxy_cache_valid line. That configuration will serve stale content happily when your origin is slow, but it has no strategy for the moment the origin is genuinely down. The directives stale-while-revalidate and stale-if-error turn a cache into a resilience layer: they let the edge keep serving a slightly older object while a refresh happens in the background, or when the backend is throwing 5xx. This tutorial wires them up correctly and, just as importantly, measures whether they are working.

How the Two Extensions Differ

Both directives extend the lifetime of a cached object past its normal expiry, but they respond to different conditions. stale-while-revalidate=30 says: for 30 seconds after the object expires, serve the stale copy immediately and trigger one background request to refresh it — client latency stays flat instead of spiking on every expiry. stale-if-error=86400 says: if the origin returns an error or times out, keep serving the stale copy for up to a day instead of propagating the failure. Together they convert a fragile cache into something that degrades gracefully.

Configuring the Cache Zone

proxy_cache_path /var/cache/nginx/data levels=1:2 keys_zone=app_cache:20m
                 max_size=2g inactive=12h use_temp_path=off;

Then in the location block that proxies to your application, apply the policy and — critically — tell Nginx when a response is cacheable at all:

location / {
    proxy_pass http://app_upstream;
    proxy_cache app_cache;
    proxy_cache_key "$scheme$host$request_uri";
    proxy_cache_valid 200 5m;
    proxy_cache_valid 404 1m;
    proxy_cache_use_stale error timeout http_500 http_502 http_503 http_504 updating;
    add_header X-Cache-Status $upstream_cache_status always;
    add_header Cache-Control "public, stale-while-revalidate=30, stale-if-error=86400";
}

The updating token in proxy_cache_use_stale is what actually implements stale-while-revalidate in Nginx: it allows one worker to refresh the object while others serve the stale copy instead of all piling onto the origin. Without it, an expiry under load creates a thundering herd.

Verifying Cache Behaviour

The X-Cache-Status header is your window into the cache. Watch it transition through its states under load:

for i in $(seq 1 20); do
  curl -s -o /dev/null -D - http://127.0.0.1/ | grep -i x-cache-status
done
# Expect: MISS then HIT; under origin failure: STALE

To test the error path, stop the application briefly and confirm the status becomes STALE rather than a 502. Tune proxy_cache_background_update on so the revalidation happens off the request path, and proxy_cache_lock on so only one request goes upstream when a key is cold. If your caching layer sits behind a CDN, remember the CDN has its own copy of these policy decisions; the edge and origin should agree on TTLs to avoid double staleness.

Common Pitfalls

Do not apply this to authenticated or personalised responses — a stale copy of a login page is a security problem, not a feature. Set proxy_no_cache and proxy_cache_bypass for cookies and Authorization headers. And size the zone honestly: a cache that evicts objects before they expire never reaches the STALE state and silently behaves as a pass-through. Monitor the cache hit ratio and max_size usage; if evictions are frequent, increase the zone rather than the TTL. For a broader picture of how this fits with the rest of a server’s tuning, the VPS guides cover related performance topics.

Stale-serving is not about showing users old content — it is about choosing to show slightly old content rather than an error page. Configured deliberately and verified with the status header, it removes the single most common cause of user-visible outages on a self-managed web VPS.

Leave a Reply