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-StatusMeaning
MISSNot cached; the response was fetched and stored
HITServed from the cache
EXPIREDA stale entry was found and the upstream was revalidated
BYPASSThe cache was skipped by proxy_cache_bypass
STALEThe 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 on is 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

SymptomCause and fix
413 Request Entity Too Largeclient_max_body_size defaults to 1m; raise it in the server or location block
A location block seems to be ignoredLongest prefix wins, ^~ stops regex checks, and regex locations are tried in order
Certificate error on one hostnameThe wrong server block matched; check server_name and default_server
504 Gateway TimeoutThe upstream is slower than proxy_read_timeout; raise it or fix the slow endpoint
Mixed content warningsThe app never sees X-Forwarded-Proto and emits http URLs
One user sees another user's pageA 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.

Server blocks and static files Reverse proxy and TLS

Last refreshed 2026-09-18.