Nginx caching cheatsheet.
proxy_cache_path
proxy_cache_path /var/cache/nginx
levels=1:2
keys_zone=app_cache:100m
max_size=10g
inactive=60m
use_temp_path=off;
Define once in http {} block.
Use in server / location
location / {
proxy_pass http://app;
proxy_cache app_cache;
proxy_cache_key "$scheme$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_lock on;
proxy_cache_revalidate on;
add_header X-Cache-Status $upstream_cache_status;
}
Cache statuses
$upstream_cache_status:
MISS: not in cache, fetched.HIT: served from cache.EXPIRED: stale, refetched.STALE: stale served (use_stale).UPDATING: stale served while update in progress.BYPASS: cache bypassed.REVALIDATED: revalidated with origin.
Cache key
proxy_cache_key "$scheme$host$request_uri$is_args$args";
proxy_cache_key "$scheme$host$request_uri$cookie_lang";
Include vary headers in key to avoid cross-contamination.
Bypass cache
proxy_cache_bypass $cookie_logged_in $http_pragma;
proxy_no_cache $cookie_logged_in $http_pragma;
Don’t cache for logged-in users.
Microcaching
Cache for very short time to absorb spikes:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=micro:10m max_size=1g inactive=10m;
location / {
proxy_pass http://app;
proxy_cache micro;
proxy_cache_valid 200 1s;
proxy_cache_use_stale updating;
proxy_cache_lock on;
}
1s cache, but cache_lock + use_stale updating prevents thundering herd.
Respect backend headers
proxy_cache_valid any 0; # default; let backend headers control
Backend sends Cache-Control: max-age=..., nginx honors it.
proxy_ignore_headers Cache-Control Expires Set-Cookie;
proxy_cache_valid 200 5m;
Or ignore + force.
Conditional GET
proxy_cache_revalidate on;
Nginx sends If-Modified-Since / If-None-Match. Backend can return 304 → cache extended.
Purging cache
Open source nginx doesn’t include purge. Workarounds:
- Delete files in cache path matching key.
- Use
ngx_cache_purgemodule (rebuild). - Use a wrapper / orchestrator.
# Find key hash
KEY="https example.com /page"
HASH=$(echo -n $KEY | md5sum | awk '{print $1}')
# Last 2 chars / next 2 chars / hash (depending on levels)
rm /var/cache/nginx/{0..f}/{0..f}/<hash>
Practical: just rm -rf /var/cache/nginx/* && nginx -s reload.
fastcgi_cache (for PHP-FPM)
fastcgi_cache_path /var/cache/nginx/fcgi levels=1:2 keys_zone=fcgi:50m;
location ~ \.php$ {
fastcgi_cache fcgi;
fastcgi_cache_key "$scheme$host$request_uri";
fastcgi_cache_valid 200 60m;
fastcgi_pass unix:/run/php/php-fpm.sock;
include fastcgi_params;
}
Static file caching
location ~* \.(css|js|woff2|jpg|png|svg|webp)$ {
expires 1y;
add_header Cache-Control "public, immutable";
access_log off;
}
sendfile + tcp_nopush
sendfile on;
tcp_nopush on;
sendfile_max_chunk 1m;
Kernel-level file send.
open_file_cache
open_file_cache max=10000 inactive=60s;
open_file_cache_valid 30s;
open_file_cache_min_uses 2;
open_file_cache_errors on;
Cache file descriptors. Big speedup for static heavy.
Vary handling
proxy_cache_key "$scheme$host$request_uri$cookie_region";
Or include Vary headers in key explicitly via map.
Gzip + cache
Gzip after caching (cache the uncompressed):
gzip_proxied any;
Avoid caching gzipped + non-gzipped variants separately.
brotli
Requires ngx_brotli module:
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/javascript application/json image/svg+xml;
Common mistakes
- Caching responses with
Set-Cookie(mixes users). - No cache lock → thundering herd.
- Cache key without query string → wrong content served.
- Caching dynamic per-user content.
- Cache path on slow disk (use SSD or tmpfs).
Read this next
If you want my caching presets, they’re at rajpoot.dev .
Building something AI-, backend-, or data-heavy and want a second pair of eyes? I do consulting and freelance work — see my projects and ways to reach me at rajpoot.dev .