How to check cache-control header

Print the response headers and keep one line: curl -s -D - -o /dev/null https://example.com/ | grep -i cache-control. Read the directives from left to right. max-age sets the lifetime in seconds for the browser, s-maxage replaces that number for shared caches only, and no-store forbids storage in either of them.

Why check this

This check belongs on staging sign-off and after every change to a cache layer, a CDN rule or a build pipeline. The failure it prevents is a release users cannot see: an unversioned app.js shipped with max-age=31536000 stays in the browser for a year, and the bug reports all say the fix appears after a hard refresh. The same directives decide whether a response holding one customer's data may sit on a shared edge, which is why the command also runs on authenticated routes.

Prerequisites

Steps

  1. Step 1.

    Read the header from the resource, with expires and age alongside it so the whole freshness picture arrives in one request.

    curl -s -D - -o /dev/null 'https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Cache-Control' | grep -i -E '^cache-control|^expires|^age'
    
    expires: Fri, 11 Sep 2026 17:38:07 GMT
    cache-control: public, max-age=3600
    age: 1126

    The expires date is already in the past while the response is still fresh. cache-control outranks expires, so the lifetime is 3600 seconds and 1126 of them are spent.

  2. Step 2.

    Look for a second number aimed at shared caches. s-maxage applies to the CDN and is ignored by the browser.

    curl -s -D - -o /dev/null 'https://api.github.com/repos/curl/curl' | grep -i -E '^cache-control|^vary'
    
    cache-control: public, max-age=60, s-maxage=60
    vary: Accept,Accept-Encoding, Accept, X-Requested-With

    Read vary in the same pass. It names the request headers that split one URL into several stored variants.

  3. Step 3.

    Check a resource meant to be revalidated before every use. max-age=0 next to must-revalidate is the shape to expect.

    curl -s -D - -o /dev/null 'https://www.cloudflare.com/favicon.ico' | grep -i '^cache-control'
    
    cache-control: public, max-age=0, must-revalidate
  4. Step 4.

    Count the header instead of printing it when a yes or no answer is enough. Zero means the server sends no directive at all.

    curl -s -D - -o /dev/null 'https://example.com/' | grep -ci '^cache-control'
    
    0

    An absent header does not mean an absent cache. That same response carries last-modified, and a cache may derive a lifetime from it without asking anyone.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | max-age=31536000, immutable | Stored for a year, never revalidated | Correct only on a filename carrying a content hash. Wrong on /app.js. | | no-store | Never written to disk or memory, anywhere | Correct on anything holding a session, a token or personal data. | | no-cache | Stored, but revalidated before every use | The response also needs etag or last-modified, or every use is a full download. | | private, max-age=600 | The browser may keep it, the CDN may not | The right pair for a per-user page served through a shared edge. | | No header at all | The cache picks a lifetime by heuristic | Set one. A number guessed from last-modified is not a contract you control. |

Common mistakes

Sign: A new build is live, yet browsers keep serving the old bundle for days.Cause: A long max-age sits on a filename that never changes. The browser has no reason to ask again until the number runs out, and a deploy cannot reach into it. Only a new URL replaces a stored response.
Sign: The team sets expires and the value appears to be ignored.Cause: When both headers are present, cache-control decides. Step 1 shows an expires date in the past on a response that is still fresh for 3600 seconds, because expires is only a fallback for caches too old to parse cache-control.
Sign: A per-user page is served to the wrong account after a CDN rollout.Cause: public with no private directive lets a shared cache store one user's response and hand it to the next visitor. Reading cache-control on an authenticated route catches this before the edge does.

Thresholds

max-age=3600 on a documentation page, max-age=10 on a marketing home page, s-maxage=60 on a public API Source: Measured on developer.mozilla.org, www.cloudflare.com and api.github.com on 2026-09-11, see the Verified block

What to check next

FAQ

What is the difference between no-cache and must-revalidate?

no-cache revalidates before every use, fresh or not. must-revalidate allows normal use inside the freshness window and forbids serving the entry once that window closes. One removes the window, the other removes the grace period after it.

What is the difference between expires and cache-control?

expires is one absolute date. cache-control carries directives plus a relative lifetime, and it wins wherever both appear. Step 1 shows a response with an expires date already past that is still fresh, because max-age=3600 decides.

How to check cache control header in chrome?

Open DevTools, Network tab, click the request, then the Headers panel and its Response Headers section. The Size column reads "(disk cache)" or "(memory cache)" when the response never left the machine.

How to check cache headers with curl?

The four commands above are the check. -D - writes headers to stdout, -o /dev/null discards the body, and grep -i keeps the line you want. No curl flag stores or reuses a response, so curl reads the contract and never exercises it.

Verified

Verified by Maks Vernycurl 8.21.0

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

basic4 minpublished updated Maks Verny