NGINX proxy_cache not caching: why responses bypass the cache
After enabling proxy_cache and defining the cache path, upstreams still take every hit. Access logs show $upstream_cache_status as BYPASS or MISS, hit rate stays near zero, and nothing appears in the error log. NGINX applies a strict request-phase and response-phase decision tree before anything enters the cache. If any condition fails, the response is never stored. The symptom looks like upstream overload, but the root cause is usually a directive, a header, or a missing validity window.
What this means
NGINX caching has two phases. The request phase decides whether to look up or bypass the cache (proxy_cache_bypass, request method, proxy_no_cache). The response phase decides whether the upstream response can be stored (Cache-Control, Set-Cookie, Expires, proxy_cache_valid, proxy_buffering). BYPASS means a request-phase rule matched. MISS on a cacheable resource usually means a response-phase rule blocked storage. MISS on every request to a static asset means the response reached the cache zone but failed the storage check.
flowchart TD
A[Client request] --> B{Method GET/HEAD?}
B -->|No| C[BYPASS or proxy]
B -->|Yes| D{proxy_cache_bypass true?}
D -->|Yes| E[BYPASS]
D -->|No| F{Cache key exists?}
F -->|Yes| G[Serve cached entry]
F -->|No| H[Fetch from upstream]
H --> I{proxy_buffering on?}
I -->|No| J[Stream to client; no store]
I -->|Yes| K{Set-Cookie present?}
K -->|Yes| L[Skip storage]
K -->|No| M{Cache-Control prohibits?}
M -->|Yes| N[Skip storage]
M -->|No| O{Expires or max-age or proxy_cache_valid?}
O -->|No| P[Skip storage]
O -->|Yes| Q[Store in cache]Common causes
| Cause | What it looks like | First thing to check |
|---|---|---|
Set-Cookie in upstream response | MISS or BYPASS; every response carries a session cookie | Response headers for Set-Cookie |
Cache-Control: no-cache, private, or no-store | MISS; upstream explicitly forbids caching | curl -I to upstream or check application headers |
Missing proxy_cache_valid | MISS; upstream sends no Expires or Cache-Control: max-age | nginx -T | grep proxy_cache_valid |
| Request method is not GET or HEAD | BYPASS; POST, PUT, PATCH, or DELETE requests | Access log method and proxy_cache_methods |
proxy_cache_bypass or proxy_no_cache evaluates true | BYPASS; query args, cookies, or headers trigger the condition | nginx -T | grep -E 'proxy_cache_bypass|proxy_no_cache' |
proxy_buffering is off | MISS; responses stream straight to client | nginx -T | grep proxy_buffering |
Vary: * or overly broad Vary header | MISS; cache refuses to store or fragments keys | Response headers for Vary |
Quick checks
# Cache status distribution (requires $upstream_cache_status in log_format)
grep -oP 'cache_status=\K[A-Z]+' /var/log/nginx/access.log | tail -10000 | sort | uniq -c | sort -rn
# Show all cache-related directives in effective configuration
nginx -T 2>/dev/null | grep -E 'proxy_cache|proxy_cache_valid|proxy_cache_bypass|proxy_no_cache|proxy_ignore_headers|proxy_buffering|proxy_cache_methods|proxy_cache_key'
# Inspect response headers from upstream directly
curl -s -I -o /dev/null -D - http://127.0.0.1:8080/ | grep -iE 'cache-control|expires|set-cookie|vary'
# Sample recent BYPASS and MISS requests to find patterns
grep -E 'BYPASS|MISS' /var/log/nginx/access.log | tail -20
# Verify cache path disk usage and health
du -sh /var/cache/nginx/ 2>/dev/null
df -h /var/cache/nginx/ 2>/dev/null
# Check for shared memory zone or cache allocation errors
grep -iE 'could not allocate|proxy_cache' /var/log/nginx/error.log | tail -20
How to diagnose it
- Confirm the cache status pattern. Parse access logs for
$upstream_cache_status. If the dominant status isBYPASS, focus on request-phase conditions. If it isMISS, focus on response-phase storage rules. - Check request-phase exclusions. Review
proxy_cache_bypassandproxy_no_cacheconditions. Look for variables like$cookie_nocache,$arg_nocache,$http_pragma, or$http_authorization. These triggerBYPASSwhen they evaluate to non-empty and non-zero. - Verify the request method. By default, only GET and HEAD are cached. If your workload includes cacheable POST requests,
proxy_cache_methodsmust explicitly include them. - Inspect upstream response headers. Hit the upstream directly with
curl -I. If you seeSet-Cookie,Cache-Control: no-cache,Cache-Control: private,Cache-Control: no-store, orVary: *, NGINX will refuse to cache unless you override withproxy_ignore_headers. - Validate
proxy_cache_valid. If the upstream does not sendExpiresorCache-Control: max-age, NGINX will not cache the response unlessproxy_cache_validis configured for the status code. - Check
proxy_buffering. Caching requires buffering. Ifproxy_buffering offis set, responses stream directly to the client and are never stored. - Audit the cache key. If
proxy_cache_keyincludes volatile arguments like session tokens or timestamps, every request generates a unique key. This appears asMISSeven though storage technically succeeds. The default key is based on scheme, proxy host, and request URI. - Check cache zone and disk health. A full cache disk or exhausted
keys_zonecan prevent new entries. Look forcould not allocate nodein the error log for zone exhaustion, and verify disk usage for disk-level write failures.
Metrics and signals to monitor
| Signal | Why it matters | Warning sign |
|---|---|---|
Cache hit rate (HIT / total cacheable) | Measures cache effectiveness | Drop >10% from baseline sustained |
BYPASS rate | Reveals request-phase exclusions | Sustained BYPASS for resources that should be cached |
MISS rate on warmed assets | Reveals response-phase storage failures | High MISS for URLs that previously returned HIT |
| Upstream request rate | Correlates upstream load with cache misses | Spike correlating with MISS/BYPASS increase |
| Cache disk usage | A full disk stops new storage | Usage >90% of max_size or partition capacity |
| Shared memory zone errors | Zone exhaustion blocks new entries | could not allocate node in error log |
Fixes
Upstream sends Set-Cookie
If the upstream unconditionally sets cookies, NGINX treats the response as uncacheable. To cache the response anyway, add proxy_ignore_headers Set-Cookie;. To prevent that cookie from reaching clients, also add proxy_hide_header Set-Cookie;. Hiding the header alone does not make the response cacheable.
Tradeoff: caching responses that contain Set-Cookie can leak session data across users. Only ignore this header for truly public content.
Upstream Cache-Control is too restrictive
When upstream sends Cache-Control: no-cache, private, or no-store, NGINX respects these by default. If the upstream is overly conservative and you control the cache TTL at the edge, add proxy_ignore_headers Cache-Control; and pair it with explicit proxy_cache_valid directives.
Tradeoff: you override the upstream application’s intent. Ensure TTLs match content freshness requirements.
Missing proxy_cache_valid
Without proxy_cache_valid, NGINX requires either an Expires header with a future date or Cache-Control: max-age to store anything. If the upstream sends neither, add a validity window:
proxy_cache_valid 200 301 302 10m;
proxy_cache_valid 404 1m;
Tradeoff: proxy_cache_valid any 5m caches every status code, including errors. Be specific.
Non-GET/HEAD methods
If you must cache idempotent POST responses, explicitly enable them:
proxy_cache_methods GET HEAD POST;
Tradeoff: POST caching is risky for non-idempotent endpoints. Use only for safe, stable payloads.
proxy_cache_bypass or proxy_no_cache conditions
Audit conditions like:
proxy_cache_bypass $cookie_nocache $arg_nocache $http_pragma;
Remove variables that are incorrectly matching your traffic. If you need authenticated caching, consider varying the cache key by a stable user identifier instead of blanket bypassing.
Tradeoff: removing bypass conditions increases the risk of serving cached private content to the wrong client.
proxy_buffering is off
Re-enable buffering:
proxy_buffering on;
NGINX cannot cache responses that are not buffered. This directive must be enabled for the cache store to receive the full response body.
Aggressive Vary headers
If the upstream sends Vary: Cookie, NGINX creates a separate cache entry per unique cookie value, which effectively fragments the cache into useless partitions. Use proxy_ignore_headers Vary; only if you can guarantee content does not actually vary by the listed headers. A better fix is to correct the upstream application to emit accurate Vary values.
Prevention
- Log
$upstream_cache_statusin your access log format from day one. Without it, cache behavior is invisible. - Set explicit
proxy_cache_validwindows for every cached location. Do not rely solely on upstreamExpiresheaders. - Review upstream response headers before enabling caching in production. Know which headers trigger exclusion.
- Keep
proxy_cache_keystable. Exclude volatile query parameters that do not affect the response. - Monitor cache hit rate and
BYPASSrate as primary indicators, not just upstream latency. - Size
proxy_cache_pathdisk andkeys_zonegenerously. Track disk usage and error logs for allocation failures.
How Netdata helps
- Charts
$upstream_cache_statusper dimension (HIT,MISS,BYPASS,EXPIRED,STALE) via theweb_logcollector, so you can see when storage stops working. - Correlates cache hit rate drops with upstream response time and connection state changes in the same timeline.
- Surfaces error log patterns like
could not allocate nodethat indicate shared memory zone exhaustion. - Monitors cache-partition disk I/O latency to rule out disk saturation.
- Tracks nginx worker file descriptors and connection slot utilization to rule out capacity cliffs that masquerade as cache misconfiguration.
Related guides
- How NGINX actually works in production: a mental model for operators
- nginx 413 Request Entity Too Large: client_max_body_size explained
- nginx 499 status code: why clients close connections before the response
- nginx 500 Internal Server Error: how to diagnose it
- nginx 502 Bad Gateway: causes and how to fix it
- nginx 503 Service Temporarily Unavailable: causes and fixes
- nginx 504 Gateway Time-out: causes and fixes
- NGINX active connections climbing: reading, writing, waiting explained
- NGINX backend cascade failure: when slow upstreams take down everything
- nginx: a client request body is buffered to a temporary file - what it means
- nginx connect() failed (111: Connection refused) while connecting to upstream
- NGINX connection exhaustion: detection, diagnosis, and prevention







