Caching and compression in Nginx
Two questions decide how fast a web page feels: how many times is the same content generated again, and how many bytes travel over the network? Caching answers the first: a copy of content that has been prepared once is stored and served to later requests without being generated again. Compression answers the second: the response is made smaller before it is sent, and the browser unpacks it.
Nginx does both with a handful of directives. This guide first explains what each one is for, then how to configure it and, most importantly, what must not be cached. The examples are general-purpose; choose values to suit your own site and test the configuration after every change.
In brief
- Browser caching stops returning visitors from downloading the same files again.
- gzip shrinks text-based responses; it is not applied to images or archives.
- proxy_cache stores the backend server's response and serves it directly to later requests.
- Logged-in and personalised pages do not belong in a shared cache.
Caution
A badly configured server cache can show one user's page to another user. Do not enable proxy_cache or fastcgi_cache until you are sure that pages involving a login, a basket or personal data are kept out of the cache.
On this page
Three tools: browser cache, compression, server cache
-
Which cache lives where?
"The cache" is not a single thing; three separate copies can be kept at different points along the path of a request:
- Browser cache: On the visitor's own device. Through response headers the server says "you may keep this file for this long", and the browser does not ask for the file again during that time.
- Server cache (proxy_cache / fastcgi_cache): On the Nginx server's disk. Acting as a reverse proxy, Nginx stores the response it received from the backend server and answers later requests for the same address without contacting the backend at all. It is shared by all visitors.
- Intermediate caches: Services such as a CDN look at the same headers and keep their own copies. See the CDN guide for details.
Compression, on the other hand, keeps no copy; it makes the response smaller while it travels over the network. The three do not replace one another; they are used together.
: Enlarge -
Browser caching for static files
CSS, JavaScript, image and font files rarely change. The
expiresdirective adds theExpiresandCache-Control: max-ageheaders to the response;add_header Cache-Controlsupplies additional values such aspublicorimmutable.What determines the lifetime is whether the file's address changes:
- Versioned file names (e.g.
app.3f9a1c.css): because the name changes whenever the content does, a very long lifetime (a year, for example) andimmutableare safe.immutabletells the browser "there is no need to revalidate this file until it expires". - Files with a fixed name (e.g.
style.css): with a long lifetime, visitors see updates late. Choose a shorter lifetime or add a version to the file name. - HTML pages: usually given
no-cache; the browser keeps a copy but checks with the server before each use.
Nginx# Versioned files (e.g. /assets/app.3f9a1c.css): the name changes whenever the content does location ^~ /assets/ { expires 1y; add_header Cache-Control "public, immutable"; access_log off; } # Other static files: the name is fixed, so use a shorter lifetime location ~* \.(?:css|js|jpg|jpeg|png|gif|webp|svg|ico|woff2)$ { expires 30d; add_header Cache-Control "public"; } # HTML pages: a copy is kept, but the server is asked before each use location / { add_header Cache-Control "no-cache"; try_files $uri $uri/ =404; }Two details deserve attention. The first block uses
^~; without it, a request for/assets/app.csswould match the regular-expressionlocationbelow and receive the shorter lifetime. Secondly,add_headerdirectives are inherited from the level above only if there is noadd_headerat all on the current level; as soon as you write a singleadd_headerinside alocation, security headers defined atserverlevel disappear there and have to be repeated. - Versioned file names (e.g.
-
Compression with gzip
In its request the browser announces, with the
Accept-Encoding: gzipheader, that it accepts compressed responses; Nginx compresses the response and sends it withContent-Encoding: gzip. Text-based content such as HTML, CSS, JavaScript, JSON and SVG becomes noticeably smaller.Nginx# Inside the http block: applies to all sites gzip on; gzip_comp_level 5; gzip_min_length 1024; gzip_vary on; gzip_proxied any; # text/html is always compressed; do not list it here gzip_types text/plain text/css text/xml application/json application/javascript application/xml application/rss+xml image/svg+xml;gzip on;switches compression on. On its own it compressestext/htmlresponses only.gzip_typeslists additional content types.text/htmlis always compressed and must not be listed (if it is, Nginx issues a duplicate type warning).gzip_min_lengthleaves responses shorter than this number of bytes uncompressed (default 20). As very small responses gain nothing, a higher threshold is usually chosen.gzip_comp_levelranges from 1 to 9 (default 1). Higher levels cost more CPU while the gain steadily diminishes; a middle value is enough for most sites.gzip_vary on;addsVary: Accept-Encodingto the response, which stops intermediate caches from mixing up compressed and uncompressed copies.gzip_proxied: by default Nginx does not compress responses to requests that arrive through another proxy (those carrying aViaheader). If a CDN or another proxy sits in front of your site, you need this directive.
Formats such as JPEG, PNG, WebP, video, ZIP and PDF are already compressed; adding them to the list only wastes CPU.
Brotli is an alternative compression method to gzip. It is not part of the standard Nginx package and requires a separate module to be installed. Writing
brotli on;without the module causes a configuration error. Even if you use Brotli, leave gzip switched on; clients that do not support Brotli will receive gzip.
: Enlarge -
Server caching with proxy_cache
When Nginx runs as a reverse proxy in front of a backend server (see reverse proxy setup), it can store responses on disk. The first request for an address goes to the backend and the response is stored (a MISS); later requests are answered straight from the cache until the entry expires (a HIT).
Nginx# Cache area: directory, key zone (10 MB), disk limit, removal of unused entries proxy_cache_path /var/cache/nginx/site levels=1:2 keys_zone=site_cache:10m max_size=1g inactive=60m use_temp_path=off; # 1 if a session cookie is present, otherwise 0 (change the cookie name to match your application) map $cookie_PHPSESSID $skip_cache { default 1; "" 0; } server { listen 80; server_name example.com; # Pages that look the same to everyone: cached location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; proxy_cache site_cache; proxy_cache_key $scheme$host$request_uri; proxy_cache_valid 200 301 10m; proxy_cache_valid 404 1m; # Request with a session: do not answer from the cache and do not store the response proxy_cache_bypass $skip_cache; proxy_no_cache $skip_cache; # Serve an expired copy if the backend cannot respond proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504; proxy_cache_lock on; add_header X-Cache-Status $upstream_cache_status; } # Admin panel, basket and account pages: no cache location ~ ^/(admin|basket|account)(/|$) { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }proxy_cache_pathmay only appear athttplevel: the directory for the files, the shared memory zone that holds the keys (keys_zone=name:size), the upper limit on disk (max_size) and the removal of entries that nobody has requested for this long (inactive).proxy_cacheenables the zone for thatlocation.proxy_cache_keydecides when two requests count as "the same". If the content also varies by something else (a language cookie, for instance), that value must be part of the key; otherwise a page generated in one language is served to someone who asked for another.proxy_cache_validis the storage time per status code. If the backend sends aCache-ControlorExpiresheader, that takes precedence.proxy_cache_bypassstops the request from being answered from the cache, andproxy_no_cachestops the response from being written to it. The condition applies when the given value is neither empty nor "0". In the example,mapsets the variable to 1 for requests carrying a session cookie; change the cookie name to match your application.proxy_cache_use_staleallows an expired copy to be served when the backend returns an error or does not respond; the site stays up during short outages.add_header X-Cache-Status $upstream_cache_status;writes the cache status into every response.
By default Nginx caches GET and HEAD requests only, and it does not store responses that contain
Set-Cookieor whoseCache-Controlheader includesprivate,no-cacheorno-store. Switching this behaviour off withproxy_ignore_headersis the most common cause of personal content leaking; if you are not sure what you are doing, do not use it.
: Enlarge -
What should not be cached?
The server cache is shared by all visitors. The rule is simple: if the response depends on who is asking, it does not go into a shared cache.
- Logged-in pages: my account, my orders, profile, personal prices or content.
- Basket and checkout steps.
- The admin panel and login pages.
- Responses containing
Set-Cookie: if cached, one user's session cookie is sent to someone else. - Form submissions (POST) and pages containing single-use tokens (CSRF tokens, for example).
- API responses that vary with the authorisation header.
The safest arrangement is to enable the cache only for addresses you have chosen explicitly (content that looks the same to everyone, such as the home page and product or article pages) and to leave admin and account addresses uncached in a separate
location. For general precautions about sessions, see the guide on login and session security.
: Enlarge
Checking that it works: X-Cache-Status
After testing and reloading the configuration, look at the response headers. Request the same address twice: you should see MISS in the first response and HIT in the second.
# Test the configuration and reload if there are no problems
sudo nginx -t && sudo systemctl reload nginx
# Static file headers: Cache-Control and Expires should appear
curl -s -o /dev/null -D - https://example.com/assets/app.3f9a1c.css | grep -i -E "cache-control|expires"
# Compression: Content-Encoding: gzip and Vary: Accept-Encoding should appear
curl -s -o /dev/null -D - -H "Accept-Encoding: gzip" https://example.com/ | grep -i -E "content-encoding|vary"
# Server cache: request the same address twice (first MISS, then HIT)
curl -s -o /dev/null -D - https://example.com/ | grep -i x-cache-status
curl -s -o /dev/null -D - https://example.com/ | grep -i x-cache-status| Value | Meaning |
|---|---|
| MISS | There was no copy in the cache; the response came from the backend (and was stored if eligible). |
| HIT | The response was served straight from the cache; the backend was not contacted. |
| BYPASS | The proxy_cache_bypass condition was met; the cache was skipped. |
| EXPIRED | The copy had expired; the response was fetched from the backend again. |
| STALE | The backend could not respond; an expired copy was served. |
| UPDATING | The old copy was served while the entry was being refreshed. |
| REVALIDATED | The backend confirmed that the expired copy is still valid. |
If you see MISS every time, look at the backend's response: it may be sending Set-Cookie or Cache-Control: no-store. Starting a session in PHP, for example, adds headers that prevent caching under the default settings. If you would rather not show the header to everyone on the live site, you can remove it once testing is finished.
If you use PHP-FPM: fastcgi_cache
When Nginx passes PHP not to another web server but directly to PHP-FPM (fastcgi_pass), the same logic is set up with the fastcgi_cache family: fastcgi_cache_path, fastcgi_cache, fastcgi_cache_valid, fastcgi_cache_bypass, fastcgi_no_cache. One important difference is that fastcgi_cache_key has no default value; it must be written explicitly.
fastcgi_cache_path /var/cache/nginx/php levels=1:2 keys_zone=php_cache:10m
max_size=512m inactive=60m;
map $cookie_PHPSESSID $skip_cache {
default 1;
"" 0;
}
server {
listen 80;
server_name example.com;
root /var/www/example.com/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
try_files $uri =404;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_pass unix:/run/php/php-fpm.sock; # the socket path varies by distribution
fastcgi_cache php_cache;
fastcgi_cache_key $scheme$request_method$host$request_uri; # has no default
fastcgi_cache_valid 200 5m;
fastcgi_cache_bypass $skip_cache;
fastcgi_no_cache $skip_cache;
add_header X-Cache-Status $upstream_cache_status;
}
}For the PHP-FPM connection itself, see the guide on Nginx and PHP-FPM.
Clearing the cache
If the content has changed but the cache still serves the old copy, there are several options:
- Keep lifetimes short: For pages that change often, a
proxy_cache_validof a few minutes is usually enough, and no clearing is needed. - Delete the files: Deleting the files in the cache directory empties the whole cache; Nginx fetches responses from the backend again on the next requests. Target only the directory you defined with
proxy_cache_path. - Change the key: Adding a version value to
proxy_cache_keyand changing it invalidates all old entries at once; the old files are removed automatically when theinactiveperiod ends. - Refresh a single address: If you tie the
proxy_cache_bypasscondition to a signal that only you can send (a request header accepted only from your own IP address, for instance), that request receives a fresh response from the backend and the entry is renewed.
# Empty the entire server cache.
# The path must be the directory you defined with proxy_cache_path (or fastcgi_cache_path); do not use any other directory.
sudo find /var/cache/nginx/site -type f -deleteTo the best of our knowledge, a ready-made "purge" feature that removes individual addresses is not built into the open-source version of Nginx; it is provided by the commercial version or by third-party modules. Check the documentation of your own package to see what your installation includes.
The browser cache, however, cannot be cleared from the server; it lives on the visitor's device. That is why versioned file names are the only reliable approach for long-lived static files.
Common mistakes
- Giving a one-year lifetime to CSS and JavaScript files whose names never change; updates do not reach visitors.
- Adding
text/htmlor image types to thegzip_typeslist. - Enabling
proxy_cachefor the whole site without checking for the session cookie. - Copying the line
proxy_ignore_headers Set-Cookie Cache-Control;without knowing what it does. - Not adding the relevant value to the cache key when content varies by language, device or currency.
- Writing an
add_headerinside alocationand not noticing that the security headers from the level above have vanished (see security headers). - Putting the cache directory somewhere the user Nginx runs as cannot write to.
Checklist
- Static files get a lifetime through
expires; long lifetimes only for versioned file names. - gzip is on;
gzip_typescontains text-based types only, withgzip_vary on. - The server cache is enabled only for addresses that look the same to everyone.
- Requests with a session cookie are excluded with
proxy_cache_bypassandproxy_no_cache. - The admin panel, basket and account pages are uncached.
- MISS and HIT have been observed with
X-Cache-Status; BYPASS appears while logged in. - How to clear the cache is written down and has been tried.
nginx -thas been run after every change.
Frequently asked questions
Why do changes to the site not show up straight away once caching is on?
Because the browser or the server is serving an old copy that has not yet expired. You can clear the server cache or shorten its lifetime; for the browser cache, the lasting solution is to use versioned file names for static files.
Should I use gzip or Brotli?
gzip ships with Nginx and works in every browser; switch it on first. Brotli requires a separate module. If you are able to install the module you can use both together; the browser picks the one it supports.
I keep seeing MISS. Why?
The most common reasons: the backend sends Set-Cookie or Cache-Control: no-store or private, the request carries a session cookie, proxy_cache_valid has not been defined, or the address contains a query parameter that changes with every request.
What is the difference between proxy_cache and fastcgi_cache?
The logic is the same; the difference is where Nginx gets the response from. If the backend is a server that speaks HTTP (proxy_pass), use proxy_cache; if it is PHP-FPM directly (fastcgi_pass), use fastcgi_cache.
Do I still need the Nginx cache if I use a CDN?
The two complement each other. A CDN serves content from locations close to the visitor, while the Nginx cache reduces the load on the application for the requests the CDN passes on to your server. If your cache headers are correct, both follow the same rules.
BYK Yazılım Support Team
This guide is written and regularly reviewed by the BYK Yazılım support team. Last updated: 4 October 2026.
Related guides
- How to set up Nginx as a reverse proxyStep-by-step setup: server block, proxy_pass, forwarded headers, WebSocket, timeouts and real client IP.
- Nginx and PHP-FPM: how PHP sites run on Nginxfastcgi_pass, SCRIPT_FILENAME, try_files, the pool concept, socket permissions and the 502 error.
- What is a CDN (content delivery network)?What a CDN is, how it is put in place, and what to watch for with real IPs, caching and origin server security.
- 502, 504 and other Nginx errors: causes and fixesWhat 502, 504, 413, 499, 403 and 404 mean, their likely causes, diagnosis from the logs and the fix.
Let us talk about your website infrastructure
BYK Yazılım builds corporate websites. Write to us with any questions about your site.
Contact us Our corporate website service