Nginx Content Caching
Introduction¶
proxy_cache lets Nginx store responses from your backend and serve repeat
requests directly — no round trip to the app. Done right it removes most load;
done wrong it serves stale or user-specific content to the wrong people.
Basic setup¶
# http {} context
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:100m
max_size=10g inactive=60m use_temp_path=off;
# server {} / location {}
location / {
proxy_pass http://backend;
proxy_cache app_cache;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status; # HIT / MISS / EXPIRED / STALE / BYPASS
}
keys_zone— shared memory for keys/metadata (100m ≈ ~800k entries); the bodies go on disk underproxy_cache_path.levels=1:2— directory nesting so one dir doesn't hold millions of files.X-Cache-Status— add this header while tuning so you can see HIT/MISS in the response.
The cache key¶
Default is $scheme$proxy_host$request_uri. Add $host if you serve multiple
hostnames. Do not include cookies or auth headers unless you mean to cache
per-user (you usually don't). Do add anything that changes the response:
proxy_cache_key "$scheme$host$request_uri$http_accept_encoding";
map $http_accept $is_json { ~application/json 1; default 0; }
# ...append $is_json if content negotiates
Deciding what to cache¶
# Never cache authenticated requests
proxy_cache_bypass $http_authorization $cookie_sessionid;
proxy_no_cache $http_authorization $cookie_sessionid;
# Cache static assets hard, HTML briefly, API not at all
location ~* \.(jpg|css|js|woff2)$ { proxy_cache_valid 200 7d; }
location = / { proxy_cache_valid 200 5m; }
location /api/ { proxy_pass http://backend; } # no proxy_cache
proxy_cache_bypass— if true, fetch from upstream (but still store the result).proxy_no_cache— if true, don't store. Set both for "always fresh for logged-in users".- Honour upstream
Cache-Control: no-store/privateby default; override withproxy_ignore_headers Cache-Control Set-Cookie;carefully.
Stale content = resilience¶
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on; # serve stale, refresh in the background
proxy_cache_lock on; # one request fills the cache, others wait (no stampede)
Now if the backend is down or slow, users get the last good copy instead of an error.
Micro-caching dynamic pages¶
Even a 1-second cache on a homepage collapses a traffic spike:
location / {
proxy_pass http://backend;
proxy_cache app_cache;
proxy_cache_valid 200 1s;
proxy_cache_lock on;
proxy_cache_use_stale updating;
}
1000 req/s to the homepage becomes ~1 req/s to the app. Only safe for content that's identical for all anonymous users and can tolerate 1s staleness.
Purging¶
Open-source Nginx has no PURGE method built in. Options:
proxy_cache_bypasswith a secret header to force a refresh of one URL.- Delete the file: the path is
md5("$cache_key")under the levels dirs — scriptable but fiddly. ngx_cache_purgethird-party module, or Nginx Plus, adds real purge.- Just use short TTLs and let it expire.
Verification and troubleshooting¶
curl -sI https://example.com/ | grep -i x-cache-status
du -sh /var/cache/nginx
nginx -T | grep -A5 proxy_cache_path
- Always
MISS— the response hasSet-Cookie(Nginx won't cache by default),Cache-Control: no-cache/no-store/private,Expiresin the past, or aVary: */Vary: Cookie. Check upstream headers;proxy_ignore_headersif appropriate. HITfor logged-in users seeing each other's data — noproxy_no_cache/proxy_cache_bypasson the session cookie, or the cache key doesn't vary on auth. Serious bug — audit immediately.- Cache directory fills the disk —
max_sizenot set or too high;inactivetoo long. Nginx's cache manager prunes tomax_sizebut lags under heavy write. - Stale content after a deploy — TTL hasn't expired; add a purge step to the deploy, or version asset URLs (hashed filenames) so new content = new key.
- Thundering herd on expiry (many MISS at once) —
proxy_cache_lock on+proxy_cache_use_stale updating. X-Cache-Status: BYPASSalways — aproxy_cache_bypassvariable is non-empty/non-zero for every request (e.g. a cookie everyone has). Narrow the condition.
Related tools and reading¶
- On-site: HTTP Header Analyzer, Redirect / cache checker.
- Related posts: How Nginx location block matching works, Varnish cache in front of a site.
Stuck on something this site can't fix?Reach out to Prabath directly on LinkedIn.More in Web & Mail Servers
Running BIND 9 as an Authoritative Server
BIND as an authoritative server: zone files, secured AXFR with TSIG, SOA serial rules, and DNSSEC inline-signing. Here's a working master/slave setup.
October 24, 2026Varnish Cache in Front of a Site
Varnish is a caching HTTP accelerator configured in VCL. Here's the request lifecycle, why you strip cookies, how bans work, and grace mode for serving stale during backend trouble.
October 24, 2026Let's Encrypt with Certbot, Including Renewal That Doesn't Break
Getting the first cert is easy; keeping renewal working for years is the part people get wrong. Webroot vs DNS-01, deploy hooks, and the failure modes.
October 24, 2026