Why a GET and not a HEAD
Most header tools send HEAD because it is cheaper. That is exactly why their results are sometimes wrong. WAFs, bot filters, and a good number of application frameworks handle HEAD differently from GET — returning 405, omitting Content-Length, skipping compression negotiation, or short-circuiting before the middleware that sets security headers ever runs. This tool sends a normal GET, follows redirects, and reports the headers on the final response, which is what a browser would see.
The caching headers, in the order they are applied
Cache-Control decides everything; the older Expires header only matters when Cache-Control is absent. The directives that actually change behaviour:
| Directive | Effect |
|---|---|
| max-age=N | Fresh for N seconds in any cache, including the browser |
| s-maxage=N | Overrides max-age for shared caches (CDN, proxy) only |
| no-cache | May be stored, but must be revalidated before reuse — not the same as no-store |
| no-store | Must not be written to disk or memory at all; the correct header for authenticated pages |
| private | Browser may cache, shared caches must not |
| immutable | Skip revalidation entirely for the lifetime — only safe on fingerprinted asset URLs |
| stale-while-revalidate=N | Serve stale for up to N seconds while refreshing in the background |
Read the full explanation of http headers checker results ↓Show less ↑
The single most common production mistake is Cache-Control: no-cache on an authenticated page, in the belief that it prevents storage. It does not — it permits storage and requires revalidation. For anything containing personal data the correct value is no-store, usually paired with private.
Vary is the other header worth reading carefully. Vary: Accept-Encoding is normal and necessary. Vary: User-Agent fragments your CDN cache into thousands of near-identical entries and effectively disables it; Vary: Cookie on a page that sets an analytics cookie does the same thing. If your CDN hit rate is inexplicably bad, this is the first header to look at.
Reading the CDN and origin headers
cf-cache-status,x-cache,x-served-by— whether the edge served a HIT, a MISS, or bypassed the cache entirely. Repeated MISS on a static asset usually means the origin is sendingno-storeor aSet-Cookiethat suppresses caching.age— how many seconds the shared cache has held this response. A large age on a page you just deployed means the purge did not happen.server,via,x-powered-by— the software in the path.x-powered-byleaks your framework and version for no benefit; remove it.alt-svc— advertises HTTP/3 availability. Its absence is why a site can be HTTP/3-capable and still never negotiate it.etagandlast-modified— the validators that make conditional requests work. A response with neither cannot be revalidated cheaply, so every expiry costs a full transfer.
Common problems and what causes them
| What you see | Likely cause | Fix |
|---|---|---|
| Deployed change not visible | Long max-age plus no purge, or high age at the edge | Purge the CDN; fingerprint asset filenames instead of relying on short TTLs |
No content-encoding | Compression off, or stripped by a proxy | Enable gzip/brotli at the edge and confirm Vary: Accept-Encoding is set |
content-type: text/plain on HTML | Wrong MIME mapping | Fix the server MIME types; with nosniff set the page will not render at all |
set-cookie on static assets | A framework session cookie applied globally | Scope the session middleware to dynamic routes — cookies suppress shared caching |
| CDN never caches anything | Over-broad Vary, or Cache-Control: private | Narrow Vary to Accept-Encoding; use s-maxage for shared caches |
| 403 or 503 from this tool but not your browser | A WAF or bot filter blocking a non-browser User-Agent | Expected — confirm with curl using the same UA |
Content-Type: text/html with no ; charset=utf-8 leaves the browser to guess, which is both a rendering bug and a historical XSS vector. Always send it explicitly.The same request with curl
curl -sL -o /dev/null -D - https://example.com
HTTP/2 200
content-type: text/html
cache-control: max-age=3077
age: 3077
server: cloudflarefor i in 1 2; do
curl -sI https://example.com | grep -Ei '^(cf-cache-status|age|x-cache):'
donecurl -sI -H 'Accept-Encoding: br, gzip' https://example.com \
| grep -Ei '^(content-encoding|vary):'For the security-relevant subset of these headers — HSTS, CSP, frame protection, nosniff, Referrer-Policy, Permissions-Policy — the security headers checker grades them against a fixed published rubric and tells you exactly what to add.