Caching, compression and traps
Cache responses safely, compress the right things, and recognise the configuration mistakes that cause most incidents.
Caching responses
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=app_cache:10m
max_size=1g inactive=60m use_temp_path=off;
server {
location /api/ {
proxy_cache app_cache;
proxy_cache_valid 200 302 10m;
proxy_cache_valid 404 1m;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_use_stale error timeout updating;
proxy_cache_bypass $http_authorization; # never cache private data
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://127.0.0.1:3000;
}
location /assets/ {
root /var/www/site;
expires 30d;
add_header Cache-Control "public, immutable";
}
}X-Cache-Status | Meaning |
|---|---|
MISS | Not cached; the response was fetched and stored |
HIT | Served from the cache |
EXPIRED | A stale entry was found and the upstream was revalidated |
BYPASS | The cache was skipped by proxy_cache_bypass |
STALE | The upstream failed, so a stale copy was served |
The cache key decides correctness. The query string is already part of $request_uri; exclude cookies and headers you do not want to fragment the cache on, and bypass it entirely for anything personalised.
Compression
gzip on;
gzip_comp_level 5; # 5 is the sweet spot; 9 costs CPU for little gain
gzip_min_length 1024; # do not compress tiny responses
gzip_vary on; # add Vary: Accept-Encoding for shared caches
gzip_types text/plain text/css application/json
application/javascript text/xml image/svg+xml;
# serve pre-compressed .gz files instead of compressing per request
gzip_static on;- Never compress images, video or archives: they are already compressed and end up slightly larger.
gzip_vary onis what stops a shared cache from handing a compressed body to a client that cannot read it.- Compression pays off for text above roughly 1 KB. Below that the headers outweigh the saving.
- The Brotli module gives another 15 to 20 percent on text but is not in every build, so check before configuring it.
Common configuration traps
| Symptom | Cause and fix |
|---|---|
| 413 Request Entity Too Large | client_max_body_size defaults to 1m; raise it in the server or location block |
| A location block seems to be ignored | Longest prefix wins, ^~ stops regex checks, and regex locations are tried in order |
| Certificate error on one hostname | The wrong server block matched; check server_name and default_server |
| 504 Gateway Timeout | The upstream is slower than proxy_read_timeout; raise it or fix the slow endpoint |
| Mixed content warnings | The app never sees X-Forwarded-Proto and emits http URLs |
| One user sees another user's page | A cacheable location is returning personalised content; add proxy_cache_bypass on cookies |
location /upload/ {
client_max_body_size 50m;
proxy_pass http://127.0.0.1:3000;
}
location ^~ /static/ { # ^~ : stop before regex locations are tried
root /var/www/site;
}
location ~* \.(css|js|png|jpg|svg)$ { # case-insensitive regex
root /var/www/site;
expires 30d;
}⚠️
Validate every change with
nginx -t and deploy it by reload, never by restart, on a host serving live traffic. Keep a known-good copy of the config so you can restore it in one command.FAQ
Do I need caching at all for a small site?
Static files benefit most: let the browser cache them with
expires. Skip proxy caching until you measure a real bottleneck, because caching hides bugs as often as it reveals them.gzip or Brotli?
Brotli usually compresses text 15 to 20 percent better at similar speed. Use it when the module exists and keep gzip as the fallback; the client negotiates both through
Accept-Encoding.Related
Server blocks and static files Reverse proxy and TLS
Last refreshed 2026-09-18.