Nginx Caching
Nginx can dramatically cut backend load and response times with two kinds of caching. Browser caching means setting headers so clients reuse static assets instead of re-downloading them. Proxy caching means storing backend responses on the Nginx server and serving them directly to subsequent clients. A well-tuned proxy cache can absorb traffic spikes, keep a site up when the backend is down (by serving stale content), and make dynamic pages feel instant.
Caching is also where subtle bugs appear: personalized pages served to the wrong user, stale content that never updates, or cache stampedes when popular entries expire. The key is deciding what is cacheable, for how long, and for whom. See caching for the general principles.
TL;DR
- Static assets: fingerprinted filenames plus
Cache-Control: public, max-age=31536000, immutable. - Proxy cache: define storage with
proxy_cache_path, enable per location withproxy_cache, and set lifetimes withproxy_cache_validor upstreamCache-Control. - The cache key (
proxy_cache_key) decides what counts as "the same response". Include everything that varies the content. - Microcaching (1–10 seconds) protects backends from bursts on dynamic but public pages.
proxy_cache_use_staleplusproxy_cache_background_updateserve stale content during errors and refreshes;proxy_cache_lockprevents stampedes.- Never cache personalized responses: bypass the cache for authenticated users or
Set-Cookieresponses.
Quick Example
Core Concepts
Browser Caching
Headers tell browsers and intermediate caches how long to reuse a response:
Best practice: fingerprinted asset URLs (app.3f9c1a.js) cached for a year as immutable, and HTML documents with short or no caching, so new deployments are picked up immediately. Nginx's expires directive sets Expires and Cache-Control: max-age, and add_header Cache-Control gives full control.
The Proxy Cache
proxy_cache_pathdefines storage: a directory, a shared memory zone for keys (keys_zone), size limits (max_size), and eviction of unused entries (inactive).proxy_cache zoneenables caching for a location.proxy_cache_validsets lifetimes per status code when upstream headers don't say otherwise. By default, Nginx honors upstreamCache-Control,Expires, andX-Accel-Expires, and doesn't cache responses withSet-CookieorCache-Control: private/no-store.$upstream_cache_status(HIT, MISS, EXPIRED, STALE, UPDATING, REVALIDATED, BYPASS) is invaluable for debugging.
Cache Keys
The default key is $scheme$proxy_host$request_uri. Everything that changes the response must be in the key, or explicitly excluded from caching:
- Host (for multi-domain servers): use
$host. - Language or device variants: include a normalized header, or honor
Varycarefully. - Query parameters:
$request_uriincludes them. Normalize or strip tracking parameters (utm_*) to improve hit rates.
Microcaching
Caching dynamic pages for just 1–10 seconds sounds pointless, but under load it's transformative. At 1,000 requests per second to the homepage, a 1-second microcache means the backend renders it about once per second instead of 1,000 times, while content is never more than a second stale. It's ideal for public, frequently accessed pages: news, product listings, status pages.
Stale Content, Locking, and Revalidation
proxy_cache_use_stale error timeout http_5xx: if the backend fails, serve the last cached copy, which keeps sites up during outages.proxy_cache_use_stale updating+proxy_cache_background_update on: serve stale content instantly while one request refreshes the entry in the background (stale-while-revalidate).proxy_cache_lock on: when an entry is missing, only one request goes to the backend while others wait, which prevents a cache stampede.proxy_cache_revalidate on: use conditional requests (If-Modified-Since,If-None-Match) to refresh expired entries cheaply.
Bypassing and Purging
proxy_cache_bypass: fetch from the backend (for example for logged-in users or?nocache=1from admins).proxy_no_cache: don't store the response.- Purging specific URLs requires NGINX Plus (
proxy_cache_purge) or the third-partyngx_cache_purgemodule. Otherwise, rely on short TTLs, or versioned URLs that change when content changes.
Compression
Compression isn't caching, but it's configured alongside it: gzip on with appropriate gzip_types (JSON, JS, CSS, SVG, HTML) and gzip_vary on. Brotli (via a module or pre-compressed .br files with brotli_static) gives better ratios for text assets. Pre-compress static assets at build time to save CPU.
Best Practices
Separate Public From Personalized Content
Design pages and APIs so public content is cacheable, and personalized parts come from separate, uncached requests or client-side fetches. Bypass the cache whenever a session cookie or Authorization header is present.
Add Cache Status to Responses and Logs
X-Cache-Status and $upstream_cache_status in access logs let you measure hit ratios and diagnose why something isn't cached.
Set Cache-Control in the Application
Let the backend declare cacheability per response (Cache-Control: public, max-age=60 or private, no-store), and have Nginx honor it. The app knows best what's safe to cache.
Put a CDN in Front for Global Traffic
Nginx caching protects your origin; a CDN caches at the edge close to users worldwide. Use both, with consistent Cache-Control headers driving both layers.
Common Mistakes
Caching Personalized Pages
Users can see each other's account pages. Never ignore Set-Cookie/private for authenticated content, and bypass the cache for sessions.
Long TTLs on Unversioned Files
Caching /app.js for a year means users run old JavaScript after deployment. Fingerprint filenames, or use short TTLs with revalidation for unversioned assets.
Forgetting Host in the Cache Key
On a server hosting several domains, a key without $host can serve one site's cached page on another.
FAQ
What's the difference between browser caching and proxy caching?
Browser caching stores responses on each user's device, controlled by response headers, and saves bandwidth for repeat visits. Proxy caching stores responses on the Nginx server, shared across all users, which reduces backend load and speeds up responses for everyone.
How long should I microcache pages?
Typically 1–10 seconds for dynamic public pages. Even 1 second collapses thousands of identical requests into one backend render under heavy load, and users rarely notice a few seconds of staleness on public content.
How do I clear the Nginx cache?
Delete files in the cache directory (all of them, or specific entries whose key hashes you compute) and reload if needed, use NGINX Plus or a purge module for targeted purges, or avoid purging altogether with versioned URLs and short TTLs.
Does Nginx respect Cache-Control headers from my app?
Yes, by default. Upstream Cache-Control, Expires, and X-Accel-Expires determine cache lifetimes, and proxy_cache_valid applies only when those are absent. Responses with Set-Cookie, private, or no-store aren't cached unless you override that behavior.
Related Topics
- Nginx — The web server overview
- Caching — Caching principles and invalidation
- Caching Layers — Browser, CDN, proxy, and application caches
- CDN — Edge caching worldwide
- Web Performance — Loading performance and caching headers
- Nginx Reverse Proxy — The proxy layer being cached