You've put your site behind a reverse proxy, turned on caching, and now you have two complaints at the same time: a user who logged in sees the login page again after a refresh; and a customer who removed a product from the cart still sees that same product in the cart. Both have the same root cause: you've cached the full page without telling the proxy which responses should never be cached at all.
Edge caching works correctly when you separate three things: public responses that everyone sees identically, private responses that belong to a single user, and responses that should never be stored. Until this separation is done, every time you turn on caching, you've created a new bug.
Why edge caching doesn't work on a dynamic site
The short answer: it does work, but not with the default configuration. The problem is that most dynamic sites don't return any meaningful cache header. Your PHP or framework sends a 200 OK response and that's it. The proxy looks, sees no directive, and decides based on its own default policy. Some proxies cache nothing and you think caching is broken; some cache everything and you think the site is broken.
The solution is for the application itself to declare the caching decision. This is done with Cache-Control headers, not with proxy settings. The proxy is only the enforcer.
The headers that actually make the decision
A public page like a product listing should return this:
Cache-Control: public, max-age=60, s-maxage=300, stale-while-revalidate=600
max-age is for the browser and s-maxage is for shared caches like a CDN or reverse proxy. Take the difference between these two seriously; if you only set max-age, the CDN won't use it and you'll think edge caching isn't enabled. The 60-second value for product listings is intentional: shorter than that means useless caching, longer means updated prices reach the user late.
And a logged-in user's page should return this:
Cache-Control: private, no-store
private says only the user's own browser may keep it, and no-store says even the browser shouldn't store it. For the cart page and user panel, choose no-store. The cost is that every visit makes a full request to the backend and you get no resource savings. Pay this cost. Caching the cart page is a saving that gets repaid by losing orders.
Reverse proxy configuration: where to bypass
In Nginx, delegate the caching decision to the application and only manually bypass the paths you're sure about. A working block:
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=edge:100m
max_size=10g inactive=24h use_temp_path=off;
server {
listen 443 ssl http2;
location / {
proxy_cache edge;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 301 302 5m;
proxy_cache_bypass $cookie_sessionid $http_authorization;
proxy_no_cache $cookie_sessionid $http_authorization;
add_header X-Cache-Status $upstream_cache_status;
proxy_pass http://backend;
}
location /cart/ { proxy_pass http://backend; }
location /account/ { proxy_pass http://backend; }
location /api/ { proxy_pass http://backend; }
}
The two lines proxy_cache_bypass and proxy_no_cache are the heart of the matter. The first says if this condition holds, don't read from cache and go straight to the backend; the second says don't store the response. If you only set the first, the user's personal response sits in the cache and gets served to the next user. This is exactly the bug that gets reported as "user A sees user B's information."
Be sure to add the X-Cache-Status header. Without it, you're just guessing. With a simple curl -I you can see whether you got a HIT or a MISS or a BYPASS:
curl -sI https://example.com/products | grep -i x-cache-status
If you always see MISS, the session cookie is probably being set on all requests, even for anonymous visitors. Check this in the browser or with curl -I without a cookie.
This is where they get it wrong
The most common mistake I've seen: the team sets the session cookie on the main domain, without separating paths. The result is that every anonymous visitor also has a session cookie, the proxy_cache_bypass condition holds for everyone, and the HIT rate stays at zero. The sign is that everything looks correctly configured, the server is under load for no reason, and nobody understands why caching isn't working. The solution: set the session cookie only after a successful login, not on the first visit.
Edge caching and CDN: the difference is where the decision is made
A reverse proxy on your own server and a CDN are both edge caches, but the decision location differs. If your traffic mostly comes from one geography and the backend is in the same region, a local reverse proxy is enough and has less complexity. If users come from multiple continents, a CDN reduces network latency but you have less control over the cache-bypass logic.
| Criterion | Local reverse proxy | CDN |
|---|---|---|
| Control over bypass logic | Full, at the config level | Limited to panel rules |
| Latency for distant users | High | Low |
| Maintenance cost | On your shoulders | Managed |
| Suited for | Regional traffic | Scattered audience |
My choice for a dynamic site with logged-in users: first get the local reverse proxy right, then put the CDN in front of it. If you do the opposite, debugging which layer cached the wrong response becomes several times harder. To get familiar with content distribution options, a content delivery network is a good starting point.
The three layers that must stay separate
When you have a dynamic site, put responses into three buckets and define a fixed policy for each bucket:
- Public and cacheable: homepage, product listings, category pages, static files.
publicwith a specifics-maxage. - Private: user panel, cart, order history.
private, no-storeand cache bypass in the proxy. - Sensitive: API routes that take a token, payment pages. Full bypass, no exceptions.
A point that's rarely mentioned: caching API responses with the POST method is wrong, but many people do it because "we get speed." If your API isn't idempotent, caching it means a financial operation might run twice or not run at all. Put the method in proxy_cache_key so GET and POST never get mixed up.
Cache purging after deployment
After every deploy, the edge cache must be purged, otherwise users see the old version. If you version static files with a hash in the name (app.a3f9c1.js), you don't need to purge and you can set max-age to one year. For HTML, targeted purging is enough:
curl -X PURGE https://example.com/products
This command only works if you've enabled the purge module in Nginx. If it's not enabled, the faster way is to include the cache version in proxy_cache_key and change its value after deployment. An environment variable that's read in the config does this without a restart.
When to turn off edge caching
If your site has fewer than a few hundred concurrent visits and all users are logged in, edge caching only adds complexity. In this case, optimizing the database and indexes is more productive. Edge caching proves its worth when the ratio of anonymous visitors to logged-in users is high and those same public pages make up a large portion of traffic.
For sites whose traffic fluctuates sharply, combining edge caching with autoscaling makes sense: caching reduces the pressure and autoscaling absorbs the remaining peaks. And if you want to know which requests actually reach the backend, set up server monitoring before any caching change, not after it.
If you're still working on a single server and want to keep caching separate from the application, a cloud infrastructure (IaaS) lets you manage the proxy layer independently of the backend. For heavier, multi-service workloads, managed Kubernetes is the next option, but don't go for it as long as a single server is holding up.
Frequently asked questions
Why do users get logged out after enabling caching?
Because the login page or homepage response was cached with a public header and the browser or CDN serves that same response to the next user. The session cookie isn't in the cached response, or it belongs to another user. The solution is for every route that depends on a session to be excluded from the cache with proxy_cache_bypass and for its response to return private, no-store.
What's the difference between max-age and s-maxage in edge caching?
max-age is the validity period of the response in the user's browser and s-maxage is its validity period in shared caches like a CDN and reverse proxy. If you only set max-age, the edge cache won't use it and you'll think caching isn't enabled. For public pages, set both together.
How do I know edge caching is actually working?
Add the X-Cache-Status header to responses and check with curl -sI whether its value is HIT or MISS. If you always see MISS, the session cookie is probably being set on all requests and the bypass condition holds for everyone. Test this with a request without a cookie.
Is caching API responses correct?
Only for GET requests and public responses that don't depend on a specific user. Caching POST or any response that depends on a token causes one user's data to be served to another user or a financial operation to run twice. Include the HTTP method in the cache key so these two never get mixed up.
Comments 0
No comments yet — be the first!