Why cache responses at the Nginx level
A cache at the Nginx level stores an already-built backend response and serves it directly on the next request, without touching PHP-FPM or the proxied application. For a high-traffic site this removes most of the load from CPU and the database: a blog post or product page that does not change every second is rendered once and then served from memory or disk.
There are two cache types with the same logic but different sources: proxy_cache caches responses received through proxy_pass (see the article on reverse proxy), while fastcgi_cache caches responses from PHP-FPM received through fastcgi_pass. Configuring both directives is almost identical.
Configuring the cache zone
The cache zone is declared once in the http block and sets where files are stored and how much memory is allocated for the key index.
proxy_cache_path /var/cache/nginx/proxy levels=1:2 keys_zone=proxy_cache:10m max_size=1g inactive=60m use_temp_path=off;
keys_zone=proxy_cache:10m is the zone name and the amount of memory for metadata, roughly 8000 keys per 1 MB. max_size=1g caps the cache size on disk, and inactive=60m removes files that have not been accessed for 60 minutes, even if they are not formally expired yet.
Using proxy_cache in a location
Inside the server block the zone is attached with the proxy_cache directive, and the response lifetime with proxy_cache_valid.
location / {
proxy_pass http://127.0.0.1:3000;
proxy_cache proxy_cache;
proxy_cache_valid 200 10m;
proxy_cache_valid 404 1m;
add_header X-Cache-Status $upstream_cache_status;
}
The X-Cache-Status header is the fastest way to debug this: values HIT, MISS, EXPIRED, and BYPASS immediately show whether the response came from the cache. Without this header, you can only guess whether the cache is working from the response time, which is far less reliable.
fastcgi_cache for PHP sites
For WordPress, Bitrix, and other CMS platforms on PHP-FPM, the cache is configured the same way, only through fastcgi_cache and with a key that accounts for the method and cookies.
fastcgi_cache_path /var/cache/nginx/fastcgi levels=1:2 keys_zone=fcgi_cache:10m max_size=1g inactive=60m;
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_cache fcgi_cache;
fastcgi_cache_key "$scheme$request_method$host$request_uri";
fastcgi_cache_valid 200 10m;
fastcgi_cache_bypass $cookie_PHPSESSID;
fastcgi_no_cache $cookie_PHPSESSID;
}
For the setup of the Nginx and PHP-FPM link itself, see the article on Nginx and PHP-FPM. Caching pages for logged-in users is usually unwanted: fastcgi_cache_bypass and fastcgi_no_cache keyed on the session cookie exclude the account area from the shared cache.
Cache parameters: what each one controls
| Directive | Purpose |
|---|---|
| proxy_cache_valid | lifetime of a response with a specific status code |
| proxy_cache_key | formula for the key used to look up a cache entry |
| proxy_cache_bypass | condition under which the cache is not read |
| proxy_no_cache | condition under which a response is not stored in the cache |
| proxy_cache_lock | one request goes to the backend, the rest wait for its result |
proxy_cache_lock on; matters at the start of cache warmup: without it, a hundred simultaneous requests to a page that is not cached yet send a hundred identical requests to the backend at once, which is called a cache stampede.
Summary: caching checklist for Nginx
- A cache zone is created with
proxy_cache_pathorfastcgi_cache_pathin the http block. - Lifetime is set separately for successful and for error responses.
- Pages with session cookies and the account area are excluded via
*_no_cacheand*_bypass. - The
X-Cache-Statusheader is added for debugging and visible in a curl response. proxy_cache_lockis enabled to avoid simultaneous requests hitting a cold cache.