On this page
A dynamic site often rebuilds the same public article for every anonymous visitor. A reverse proxy cache can reuse that response, reducing origin work without changing the publishing application. The hard part is deciding which responses are safe to share.
This guide implements a deliberately conservative Nginx cache and explains where Varnish fits. You will test cache misses, hits, private sessions, and publishing freshness before exposing the configuration to real traffic.
Define a cacheable response before chasing speed
Start with public, anonymous HTML, not account dashboards or checkout pages. A shared cache is a security boundary: returning one user's personalized response to another user is more serious than a slow page.
Use the API Response Latency & Status Tester below to compare a public endpoint before and after caching. It measures requests from your browser, so network distance and browser restrictions affect results. It cannot independently prove an origin cache hit or replace authenticated testing.
Write down the pages eligible for caching, acceptable staleness, and how editors expect updates to appear. Establish a baseline during ordinary traffic and keep a bypass path available for troubleshooting. For broader bottleneck isolation, use the website speed audit workflow.
Choose one reverse proxy layer
Nginx can terminate TLS, forward requests, and maintain a disk-backed proxy cache within one service. Varnish offers a specialized caching engine and VCL policy language, often behind a separate TLS terminator. Either can work; adding both immediately makes cache ownership and invalidation harder to reason about.
The Nginx documentation describes proxy-cache directives, while the Varnish documentation explains its request lifecycle and VCL. Do not translate directives between them by name: their defaults and policy hooks differ.
This example uses Nginx in front of an HTTP application bound to 127.0.0.1:8080. It assumes an existing, working HTTPS virtual host for example.com. Keep certificate configuration intact and insert the relevant directives into that host rather than replacing your TLS setup.
Ensure the origin cannot be reached publicly around the proxy. Restrict its listener or firewall access, and configure the application to trust forwarded headers only from your proxy. Otherwise a client may spoof scheme or client-address metadata.
Reserve a small cache volume initially and check disk permissions. The Nginx worker must be able to create files there. A cache that competes with database storage can make an already overloaded server less reliable.
Configure conservative cache eligibility
Put this first block inside Nginx's http context. It defines storage and three independent reasons to skip caching. Any nonempty cookie bypasses this initial policy; that sacrifices hit rate for easier privacy verification.
proxy_cache_path /var/cache/nginx/public levels=1:2
keys_zone=public_cache:10m max_size=512m inactive=30m use_temp_path=off;
map $http_cookie $has_cookie {
default 1;
"" 0;
}
map $http_authorization $has_auth {
default 1;
"" 0;
}
map $request_uri $private_path {
default 0;
~^/(wp-admin|wp-login.php|account|cart|checkout)(/|?|$) 1;
}The URI expression is an example, not a complete inventory of your application's private routes. Include preview URLs, API endpoints, administrative aliases, and any site-specific personalization paths before enabling the cache.
Add the following location to the existing HTTPS server. If your configuration already has a matching location, merge deliberately rather than defining duplicate locations.
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_cache public_cache;
proxy_cache_methods GET HEAD;
proxy_cache_key "$scheme|$host|$request_uri";
proxy_cache_bypass $has_cookie $has_auth $private_path;
proxy_no_cache $has_cookie $has_auth $private_path;
proxy_cache_valid 200 60s;
proxy_cache_lock on;
add_header X-Cache-Status $upstream_cache_status always;
}Bypass and no-cache are complementary: bypass prevents reading an existing entry, while no-cache prevents storing the response. Both are necessary for request-side exclusions. Keeping the query string in the key avoids mixing different requested resources, although arbitrary query parameters can fragment the cache.
Nginx normally honors response controls such as Cache-Control, Expires, Set-Cookie, and Vary. Do not add proxy_ignore_headers to force sensitive responses into storage. The configured validity is a fallback for eligible responses, not an instruction to override every origin policy.
Verify hits and private-response isolation
Run a configuration test before reloading. These commands assume a systemd-based server and an authorized public endpoint you control.
sudo nginx -t && sudo systemctl reload nginx
curl -sS -D - -o /dev/null https://example.com/public-article/
curl -sS -D - -o /dev/null https://example.com/public-article/
curl -sS -D - -o /dev/null -H 'Cookie: test_session=placeholder' https://example.com/public-article/
curl -sS -D - -o /dev/null -H 'Authorization: Bearer PLACEHOLDER' https://example.com/public-article/A cold eligible request should usually show MISS, followed by HIT when the origin response permits storage. Cookie and authorization tests should show bypass behavior. A placeholder header tests routing policy, not the security of an actual logged-in session.
Use two separate test accounts to verify real personalization. Check response bodies as well as headers, then test redirects, error responses, searches, previews, and logout. A cache status header is useful evidence but is not proof that every application route is safe.
If everything misses, inspect origin cache headers before changing the proxy. A plugin may set cookies on every request or deliberately mark HTML private. Resolve that policy at its source instead of disabling safeguards. See MDN's HTTP documentation for caching semantics.
Operate for freshness, not just hit rate
A short TTL is a reasonable starting point for an editorial site. Cache invalidation must cover changed pages, feeds, category archives, and any homepage listing affected by publication. Do not assume an application's purge plugin supports your particular proxy configuration or that open-source Nginx includes every commercial purge feature.
Revisit the latency tester after confirming hits, and compare equivalent public requests. Also watch origin CPU, error rate, cache storage, and publication delay. Use the web hosting category for adjacent capacity and monitoring workflows.
Keep rollback simple: disable caching directives, test configuration, and reload while preserving ordinary proxying. Test the uncached origin under expected traffic so maintenance or a cache flush does not become an outage.
Watch cache-key cardinality during the trial. Tracking parameters can produce many entries for otherwise identical content, while removing meaningful parameters can serve the wrong page. Normalize only parameters whose application behavior you have verified. Never collapse search queries, language selections, or preview tokens merely to improve the hit ratio.
Define a freshness acceptance test for editors: publish a harmless change, request the affected pages anonymously, and record when the update appears. Repeat after a proxy reload and after a cold start. This makes cache behavior an observable publishing requirement rather than an undocumented infrastructure assumption.
Key takeaways
- Cache public content first and exclude personalized routes, cookies, and authorization explicitly.
- Respect origin policy rather than forcing every successful response into shared storage.
- Verify hits, real authenticated isolation, and publication freshness before increasing coverage or TTLs.
- Keep the origin protected, observe its uncached capacity, and maintain a tested rollback path.
Reach designers & developers at work
Put your product next to the tools people use every day. One sponsor per placement, no tracking pixels.
Become a sponsor