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:

  1. In the http block, define where the cache lives and how large it may grow with proxy_cache_path
  2. In a location block, turn that cache on with proxy_cache and 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 GET and HEAD (the default of proxy_cache_methods is GET HEAD). You do not need a separate rule to exclude POST
  • Backend responses that contain a Set-Cookie header
  • Responses whose Cache-Control contains no-cache, no-store or private, or whose Expires is 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 nor 0, the cache is not read and the request goes to the backend
  • proxy_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_valid time 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_size and check usage with du -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)

お気軽にご相談ください

お見積りへ お問い合わせへ
▲