Skip to content
Tyrone Software
Web Development

Vary: Cookie quietly destroys your CDN hit rate

Abstract illustration for the article Vary: Cookie quietly destroys your CDN hit rate

The hit rate on a marketing page is close to zero, although the page is identical for everyone. The first place to look is not the CDN configuration. It is the response headers.

How the cache key grows

A cache decides whether it may reuse a stored response by comparing the request headers named in Vary. With Vary: Cookie, the entire Cookie header becomes part of the key. Two visitors with different analytics cookies get two cache entries, and since analytics cookies are unique per visitor, almost every request is a miss.

Table showing that different cookie values create separate cache entries
The page is identical in all four cases, but the cache treats them as four different responses.

Vary: Accept-Encoding is harmless, because there are only a handful of values. Vary: User-Agent is nearly as bad as Vary: Cookie, because there are thousands of distinct strings. Vary: * tells caches not to reuse the response at all.

How it gets there without anyone asking

Django's session middleware adds Vary: Cookie to any response where the code touched request.session. That includes indirect access: reading request.user in a template context processor is enough, because the authentication middleware loads the user from the session. One innocent {% if user.is_authenticated %} in the base template, and every page on the site now varies on cookies.

The fix is structural. Keep the cacheable pages free of session access, or serve the personalised part (the login link, the cart count) from a small separate request that is not cached.

Checking and containing it

curl -sI https://example.com/pricing | grep -i -E 'vary|cache-control|age'

Some CDNs ignore most Vary values, others honour them, and some let you strip cookies from the cache key for chosen paths. Read what your provider does instead of assuming, and test with two different cookie values: the second request should show an Age header if it was a hit.

Reading the hit or miss straight from the response

Most CDNs and reverse proxies add their own header saying what happened: Cloudflare uses CF-Cache-Status (HIT, MISS, DYNAMIC, BYPASS), Fastly and Varnish commonly expose X-Cache or X-Served-By, and nginx can be configured to add X-Cache-Status from $upstream_cache_status. Watching that header while you send the same request twice is a faster way to confirm a fix worked than staring at aggregate hit-rate graphs, which take time to move and mix in traffic you have not touched yet.

curl -sI https://example.com/pricing -H 'Cookie: _ga=A1' | grep -i cf-cache-status
curl -sI https://example.com/pricing -H 'Cookie: _ga=A1' | grep -i cf-cache-status

If the second request is not a hit even with the identical cookie, something else is also varying the key: check for Set-Cookie on the response (some CDNs treat a response that sets a cookie as inherently private), and check whether the CDN's own cache key configuration already strips cookies for this content type before your Vary header is even considered. Many CDNs let you configure the cache key explicitly, ignoring some or all of Vary, which is often the more reliable fix than chasing every place a cookie gets touched in the application.

Keep reading

Technologies

  • Python
  • PHP
  • Laravel
  • Django
  • PostgreSQL
  • MySQL
  • HTML5
  • CSS3
  • Bootstrap