2026年11月13日
2026年11月13日
Nginx proxy_cache Setup: Config, Bypass Rules, Purging, HIT Checks
This article is a localized edition of the Japanese original. Read the Japanese original
Nginx's proxy_cache stores responses from the backend it proxies to (Apache behind it, PHP-FPM apps, application servers and so on) on disk, and answers the next request for the same URL without asking the backend again. Setup takes two steps:
- In the
httpblock, define where the cache lives and how large it may grow withproxy_cache_path - In a
locationblock, turn that cache on withproxy_cacheand decide what to store and for how long
The configuration in this article was checked with nginx -t and live requests on Nginx 1.30.6 (the official nginx.org Docker image) and Nginx 1.26.3 on Debian 13 (as of November 2026). The meaning of each directive follows the official documentation (ngx_http_proxy_module).
The complete configuration
Here is the finished configuration first. Put it in the http block of /etc/nginx/nginx.conf or in a file that block includes (/etc/nginx/conf.d/*.conf with the official packages, and also under /etc/nginx/sites-enabled/ with the Debian/Ubuntu packages).
# goes in the http block
proxy_cache_path /var/cache/nginx/proxy
levels=1:2
keys_zone=MAIN_CACHE:10m
max_size=1g
inactive=60m
use_temp_path=off;
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache MAIN_CACHE;
proxy_cache_key $scheme$host$request_uri;
proxy_cache_valid 200 301 302 10m;
proxy_cache_valid 404 1m;
add_header X-Cache-Status $upstream_cache_status always;
}
}
The listen 443 ssl http2; form was deprecated in Nginx 1.25.1 in favor of a separate http2 on; line. Older versions do not accept http2 on;, so check your version with nginx -v.
When you are done editing, test the syntax and then reload.
sudo nginx -t
sudo systemctl reload nginx
The proxy_cache_path parameters
proxy_cache_path can only be used in the http block.
| Parameter | Meaning |
|---|---|
/var/cache/nginx/proxy |
Directory for the cache files. The user Nginx worker processes run as must be able to write to it |
levels=1:2 |
Subdirectory levels, so files are not all piled into one directory |
keys_zone=MAIN_CACHE:10m |
Name and size of the shared memory zone that holds cache keys and metadata. The official docs say 1 MB holds about 8,000 keys |
max_size=1g |
Upper limit for the cache on disk. Above it, the cache manager process removes the least recently used data |
inactive=60m |
Cached data not accessed for this long is removed even if it is still fresh (default 10 minutes) |
use_temp_path=off |
Write backend responses straight into the cache directory instead of going through a temporary directory |
Without max_size, the cache keeps growing as long as there is free disk space. Set a limit that fits your disk.
Main directives in the location block
| Directive | Meaning |
|---|---|
proxy_cache MAIN_CACHE |
Use the cache named in keys_zone |
proxy_cache_key |
The key that distinguishes cached items. The default is $scheme$proxy_host$request_uri |
proxy_cache_valid 200 301 302 10m |
How long to keep each status code. If you give only a time, only 200, 301 and 302 responses are cached |
add_header X-Cache-Status $upstream_cache_status always |
Expose in a response header whether the cache was used (for checking) |
The default key's $proxy_host is the host name written in proxy_pass. If several domains are routed to one backend, different domains would end up with the same key, so the example uses $host (the domain of the request) instead.
What is not cached by default
As the official documentation states, the following responses are not stored even without extra settings.
- Requests other than
GETandHEAD(the default ofproxy_cache_methodsisGET HEAD). You do not need a separate rule to exclude POST - Backend responses that contain a
Set-Cookieheader - Responses whose
Cache-Controlcontainsno-cache,no-storeorprivate, or whoseExpiresis in the past - Responses with
Vary: *
In the test environment as well, POST requests got no X-Cache-Status header, and a URL that returns Set-Cookie stayed MISS no matter how often it was requested.
Ignoring the backend’s Cache-Control
If the application always sends Cache-Control: no-cache and nothing gets cached, you can ignore that header with proxy_ignore_headers.
proxy_ignore_headers Cache-Control Expires;
With this in place, pages where the backend deliberately sends no-store, such as checkout pages or account pages, also become cacheable. Combine it with the bypass rules in the next section, and confirm that pages whose content differs per person are not stored before you rely on it. Avoid ignoring Set-Cookie too (proxy_ignore_headers Set-Cookie), because you could end up handing someone else's session cookie to other visitors.
Bypassing the cache for logged-in users and admin pages
Bypass conditions are easier to read when you build variables with map instead of stacking if blocks, and they can all live in the http block. The following example is for WordPress.
# goes in the http block
map $http_cookie $skip_cache_cookie {
default 0;
~*wordpress_logged_in_ 1;
~*wp-postpass_ 1;
~*comment_author_ 1;
}
map $request_uri $skip_cache_uri {
default 0;
~^/wp-admin/ 1;
~^/wp-login\.php 1;
~^/wp-json/ 1;
}
# goes inside location /
proxy_cache_bypass $skip_cache_cookie $skip_cache_uri;
proxy_no_cache $skip_cache_cookie $skip_cache_uri;
proxy_cache_bypass: if any of the values is neither empty nor0, the cache is not read and the request goes to the backendproxy_no_cache: under the same condition, the backend response is not saved to the cache
Use both with the same conditions. With bypass alone, a page fetched while logged in can be saved to the cache and shown to visitors who are not logged in.
For applications other than WordPress, replace the cookie names that indicate a login and the admin paths with those of your application. For a more detailed WordPress setup, see How to speed up WordPress with Nginx proxy_cache.
Checking that the cache works
Request the same URL twice and look at X-Cache-Status.
curl -s -o /dev/null -D - https://example.com/sample-page/ | grep -i x-cache-status
curl -s -o /dev/null -D - https://example.com/sample-page/ | grep -i x-cache-status
In the test environment the first request returned MISS and the second HIT, while requests with a login cookie and anything under /wp-admin/ returned BYPASS.
| Value | Meaning |
|---|---|
MISS |
Not in the cache; fetched from the backend |
HIT |
Served from the cache |
BYPASS |
Matched a proxy_cache_bypass condition, so the cache was not used |
EXPIRED |
The item had expired, so it was fetched again from the backend |
STALE |
An expired item was returned as is (because of proxy_cache_use_stale) |
UPDATING |
An expired item was returned because it is being updated |
REVALIDATED |
With proxy_cache_revalidate on, the backend was asked whether the item changed and it was reused |
If it stays MISS however many times you request it, check whether the response carries Set-Cookie, Cache-Control: no-cache or similar headers.
curl -s -o /dev/null -D - https://example.com/sample-page/
add_header is not inherited from the enclosing block if the current block has even one add_header of its own. If your server block sets security headers, adding X-Cache-Status in a location drops them, so repeat the headers you need in the location block as well.
Measuring the hit ratio in the access log
Logging $upstream_cache_status lets you count how many requests were served from the cache. log_format goes in the http block.
log_format cache '$remote_addr [$time_local] "$request" $status '
'cache=$upstream_cache_status rt=$request_time';
access_log /var/log/nginx/cache.log cache;
grep -o 'cache=[A-Z-]*' /var/log/nginx/cache.log | sort | uniq -c
Requests that did not go through the cache (for example static files Nginx served itself) show up as cache=-.
Deleting cached items
Delete everything
Delete the contents of the cache directory. You do not need to stop Nginx.
sudo find /var/cache/nginx/proxy -type f -delete
Cache files have no extension, so a condition such as -name "*.cache" finds nothing.
Delete a single item
A cache file is named after the MD5 hash of its cache key. With levels=1:2, the last character of the MD5 hash and the two characters before it become the directory names. If the key is $scheme$host$request_uri, the key for https://example.com/sample-page/ is httpsexample.com/sample-page/.
KEY='httpsexample.com/sample-page/'
HASH=$(printf '%s' "$KEY" | md5sum | cut -d' ' -f1)
FILE="/var/cache/nginx/proxy/${HASH: -1}/${HASH: -3:2}/$HASH"
sudo grep -a '^KEY:' "$FILE" # check that the key matches
sudo rm -f "$FILE"
This script is for bash (${HASH: -1} is bash syntax). Near the top of each cache file there is a KEY: line, so you can use it to confirm that you built the key correctly. In the test environment the next request after deletion was a MISS, and the page was stored again.
If you are not sure of the key, you can also search the file contents.
sudo grep -rla 'KEY: httpsexample.com/sample-page/' /var/cache/nginx/proxy
Delete by URL (the ngx_cache_purge module)
proxy_cache_purge is not part of open source Nginx. You need the third-party ngx_cache_purge module. On Debian 13 it is installed with the libnginx-mod-http-cache-purge package.
sudo apt install libnginx-mod-http-cache-purge
# goes in the server block (before location /)
location ~ ^/purge(/.*)$ {
allow 127.0.0.1;
deny all;
proxy_cache_purge MAIN_CACHE $scheme$host$1;
}
The second argument must be a key in the same form as proxy_cache_key. In a location without proxy_pass, $proxy_host is empty, so if your key uses $proxy_host the purge will not match anything.
curl -s http://127.0.0.1/purge/sample-page/ -H 'Host: example.com'
In the test environment (Debian 13, Nginx 1.26.3) this printed Successful purge, and the next request was a MISS. Purging the same URL again right away returns an error status. The commercial NGINX Plus has a directive with the same name, but its syntax is different.
Handling backend failures and simultaneous requests
# goes inside location /
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_lock on;
proxy_cache_lock_timeout 5s;
| Directive | Meaning |
|---|---|
proxy_cache_use_stale |
Return an expired cached item when the backend returns an error or times out. updating also returns the old item while it is being refreshed |
proxy_cache_background_update on |
Return the expired item and refresh it in the background. Has no effect unless proxy_cache_use_stale includes updating |
proxy_cache_lock on |
When a key is not cached yet, only one request goes to the backend and the others wait for it to be stored |
proxy_cache_lock_timeout 5s |
How long to wait. Requests that wait longer go straight to the backend (and that response is not cached) |
These settings soften the moment when a cached item expires during a traffic spike and a flood of requests hits the backend at once.
Things to watch
- Caching works per URL. Pages whose content depends on login state, language or device will show content meant for someone else unless you bypass them or add the condition to the key.
- After you update an article, the old content is served until the
proxy_cache_validtime passes. On sites that update often, shorten the time or decide on a routine of purging on publish. - The cache directory consumes disk space. Set
max_sizeand check usage withdu -sh /var/cache/nginx/proxy.
The reverse proxy setup itself is covered in How to set up Nginx as a reverse proxy, and how to write map in How to use the Nginx map module.
Questions about WordPress? The publisher of this site is based in Japan and replies in Japanese. Contact (Japanese)