How to Debug Website Caching Issues Using HTTP Headers
Stale content complaints and mysterious re-downloads almost always trace back to a handful of caching headers. Here's how to read them correctly.
Cache-Control: The Core Directive
Cache-Control is the primary header governing caching behavior, and most caching bugs trace back to a misconfigured or missing value here. Key directives:
| Directive | Meaning |
|---|---|
no-store | Never cache this response anywhere |
no-cache | Can be cached, but must revalidate with the server before using the cached copy |
max-age=N | Cache is considered fresh for N seconds before revalidation is needed |
public | Can be cached by shared caches (CDNs, proxies), not just the browser |
private | Only the end user's browser may cache it — not shared caches or CDNs |
A common bug: setting private on content meant to be CDN-cached, causing the CDN to bypass its cache entirely and hit your origin server on every request.
ETag & Validation-Based Caching
ETag is a unique fingerprint for a specific version of a resource. On a follow-up request, the browser sends the stored ETag back via If-None-Match; if it still matches, the server responds with a lightweight 304 Not Modified instead of re-sending the full content. If you're seeing full re-downloads when you expected a 304, check whether the ETag value is actually stable across requests — some misconfigurations (like including a server instance ID or timestamp in the ETag) cause it to change on every request even when content hasn't, defeating validation entirely.
Last-Modified
A simpler, less precise alternative to ETag — a timestamp of when the resource was last changed. The browser sends it back via If-Modified-Since on follow-up requests. Less precise than ETag (only second-level granularity, and can't distinguish content changes that don't affect the modification timestamp), but simpler to implement correctly and still widely used, often alongside ETag as a fallback.
The Vary Header Trap
Vary tells caches which request headers should be factored into cache key uniqueness — commonly Vary: Accept-Encoding to cache compressed and uncompressed versions separately. A frequent, hard-to-spot bug: setting Vary: User-Agent or similar high-cardinality headers, which can fragment cache effectiveness dramatically since nearly every visitor has a slightly different User-Agent string, causing far more cache misses than intended.
CDN-Specific Cache Headers
Most CDNs add their own diagnostic headers indicating cache status directly — Cloudflare's CF-Cache-Status (HIT/MISS/EXPIRED/BYPASS), Fastly's X-Cache, and similar equivalents elsewhere. These are often the fastest way to confirm whether a request actually hit cache or went to origin, without needing to reason through Cache-Control logic manually.
A Practical Debugging Workflow
Check Cache-Control First
Confirm the directive present matches your intent — public vs private, and the max-age value, are the most common misconfiguration points.
Check CDN Cache Status Header
If using a CDN, its cache-status header tells you immediately whether the request hit cache or went to origin.
Verify ETag Stability
Request the same resource twice and confirm the ETag value is identical if content hasn't changed — instability here defeats validation caching entirely.
Check for an Overly Broad Vary Header
A high-cardinality Vary value can silently fragment your cache into thousands of near-unique entries.
Use our HTTP Headers Checker to quickly pull the full caching-related header set for any URL without needing to open browser DevTools.
FAQs
ToolsNovaHub guides are researched against primary sources (RFCs, vendor docs) and kept up to date as standards change. Spotted an error? Let us know.
📋 Related Tools & Guides Comparison
| Resource | Type | Link |
|---|---|---|
| HTTP Headers Checker | Tool | Open Tool → |
| Security Headers Checker | Tool | Open Tool → |
| Redirect Checker | Tool | Open Tool → |
| HTTP Headers: The Complete Guide | Guide | Read Guide → |
| High Latency: Causes & Fixes | Guide | Read Guide → |