Nginx proxy cache doesn’t actually reduce load on your origin servers by caching data; it reduces load by lying to your origin servers.
Let’s see it in action. Imagine you have an origin server serving a static index.html file.
http {
# ... other http settings ...
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=my_cache:10m max_size=1g inactive=60m use_temp_path=off;
server {
listen 80;
server_name example.com;
location / {
proxy_pass http://origin_server_ip:8080;
proxy_cache my_cache;
proxy_cache_valid 200 302 10m; # Cache successful responses for 10 minutes
proxy_cache_key "$scheme$request_method$host$request_uri";
add_header X-Cache-Status $upstream_cache_status;
}
}
}
# On your origin server (e.g., a simple Python HTTP server)
# python3 -m http.server 8080
When a client requests /, Nginx first checks its cache (my_cache). If it’s not there, it forwards the request to http://origin_server_ip:8080. The origin server responds, Nginx caches it (if the status code is 200 or 302), and sends it to the client. The client sees X-Cache-Status: MISS.
Now, if another client requests / within 10 minutes, Nginx serves it directly from /var/cache/nginx/. The origin server never sees this request. The client sees X-Cache-Status: HIT. If a third client requests it after 10 minutes, Nginx checks the cache, finds it expired, and then forwards the request to the origin, resulting in another MISS.
This mechanism is powerful because it means that for every HIT, your origin server does zero work. It doesn’t have to read a file, process a request, or generate a response. It’s completely bypassed. The primary problem this solves is preventing your origin from being swamped by repetitive requests for content that doesn’t change frequently.
The core of Nginx caching is the proxy_cache_path directive. This tells Nginx where to store cached files (/var/cache/nginx), how to organize them (levels=1:2 creates a two-tier directory structure like /var/cache/nginx/c/29/...), the name of the shared memory zone for storing cache keys and metadata (keys_zone=my_cache:10m allocates 10MB of RAM for this), the maximum size of the cache (max_size=1g), and how long an item can remain in the cache without being accessed (inactive=60m). use_temp_path=off prevents Nginx from writing to a temporary location first, which can be slightly faster but might cause issues if disk space is tight during writes.
Inside your server block, proxy_cache my_cache; enables caching using the zone defined earlier. proxy_cache_valid 200 302 10m; is crucial: it tells Nginx to cache responses with HTTP status codes 200 and 302 for 10 minutes. You can specify different durations for different codes. proxy_cache_key "$scheme$request_method$host$request_uri"; defines how Nginx generates a unique key for each cached item. This string concatenation ensures that requests for http://example.com/ and https://example.com/ (if both are configured) are treated as distinct cache entries, as are GET /page and POST /page. The add_header X-Cache-Status $upstream_cache_status; line is purely for debugging, injecting a header into the client’s response indicating whether the item was a HIT, MISS, EXPIRED, or BYPASS.
What most people don’t realize is that proxy_cache_bypass and proxy_cache_unless are incredibly powerful for fine-grained control. You can use them to prevent caching for specific conditions. For instance, if you want to bypass the cache for requests coming from a specific IP range or containing a particular cookie:
proxy_cache_bypass $http_cookie ~ "debug_session";
proxy_cache_bypass $arg_nocache;
This means if the debug_session cookie is present, or if ?nocache=true is in the URL, Nginx will not even check the cache; it will go straight to the origin. This is often more efficient than letting Nginx fetch from cache and then discarding it.
The next concept you’ll grapple with is cache invalidation – determining how to proactively remove stale content from the cache when your origin data does change, rather than just waiting for inactive or proxy_cache_valid timeouts.