# HTTP Caching Headers, Explained With curl

Caching is one of those topics everyone nods at and almost nobody has watched happen. I spent ten minutes pointing curl at real sites and reading the headers they actually send. Everything below is output I captured on 10 November 2026, around 06:06 UTC, from this machine. The commands are the whole method:

```bash
curl -sI https://example.com
curl -s -D - -o /dev/null https://dev.to
curl -sI https://github.com
curl -sI https://nodejs.org
curl -sI https://aheadkit.me
```

(`-I` sends a HEAD request; `-D -` dumps the response headers of a GET. The first line of each dump, `HTTP/1.1 200 Connection Established`, is my network's egress proxy shaking hands — not the site. Ignore it.)

## The easy one: [example.com](http://example.com)

```plaintext
HTTP/1.1 200 OK
Date: Tue, 10 Nov 2026 06:06:31 GMT
Content-Type: text/html; charset=utf-8
Age: 8
Server: cloudflare
last-modified: Thu, 05 Nov 2026 21:17:41 GMT
cf-cache-status: HIT
```

This is the textbook case, and it's worth reading slowly. `cf-cache-status: HIT` means Cloudflare served this from its cache, not from the origin. `Age: 8` says the cached copy is 8 seconds old. `last-modified` says the file itself hasn't changed since 5 November — five days ago.

## The careful one: [dev.to](http://dev.to)

```plaintext
Cache-Control: public, no-cache
Etag: W/"5bbbdd4a98cfe9a0ed70ff9eb4c71bac"
Via: 1.1 heroku-router, 1.1 varnish, 1.1 varnish
X-Accel-Expires: 60
Age: 5
Server: Heroku
```

[dev.to](http://dev.to)'s homepage is the opposite of [example.com](http://example.com): it changes constantly, and it looks different logged in versus logged out (`Vary: Accept-Encoding, X-Loggedin` says exactly that). So the header says `public, no-cache` — a cache may store the response, but it must check with the origin before serving it. That's what `no-cache` actually means, and it's the most misunderstood directive in HTTP: it does not mean "don't cache." It means "cache, but revalidate every time."

The revalidation tool here is the `Etag`, a fingerprint of this exact version of the page. `W/` marks it weak — it can change without the bytes changing — but it's still a fingerprint. A cache that holds this page sends the fingerprint back:

```bash
curl -s -o /dev/null -w "HTTP %{http_code} | %{size_download} bytes\n" \
  -H 'If-None-Match: W/"5bbbdd4a98cfe9a0ed70ff9eb4c71bac"' https://dev.to
```

Result: `HTTP 200 | 295411 bytes downloaded`. No 304. The fingerprint didn't match — the homepage had already changed in the minutes since I captured the ETag, or [dev.to](http://dev.to) doesn't honour conditional requests on a page this dynamic. Either way, the full 295 KB came down again. `X-Accel-Expires: 60` adds the other half of the story: Varnish, the cache in front of [dev.to](http://dev.to), treats the page as fresh for only 60 seconds.

## The strict one: [github.com](http://github.com)

```plaintext
ETag: W/"6a7d563f0b57b2f121844a782b69ce91"
Cache-Control: max-age=0, private, must-revalidate
```

GitHub's homepage is personal — your feed, your notifications — so `private` forbids shared caches (CDNs, proxies) from storing it at all. `max-age=0` says it's stale the instant it arrives, and `must-revalidate` forbids serving that stale copy without asking the origin first. The ETag is still there, so revalidation can be cheap: send the fingerprint, get a 304 if nothing changed, download nothing. The combination is the standard shape for "this page is yours, and it's always fresh-checked."

## The one that refuses: [nodejs.org](http://nodejs.org) and [aheadkit.me](http://aheadkit.me)

[nodejs.org](http://nodejs.org) answers its homepage with a 307 redirect and `CF-Cache-Status: DYNAMIC` — Cloudflare won't cache it at all, because the response is generated per request. [aheadkit.me](http://aheadkit.me), my own site, answered with `X-Vercel-Cache: MISS` on the first hit — Vercel's edge cache didn't have it yet — and the response carries `Cache-Control: public, max-age=0, must-revalidate`, the same "check every time" shape as GitHub.

The lesson from both: a cache header is a promise about freshness, and some pages promise nothing. A 307 with DYNAMIC is the server saying "don't even try to remember this one."

## Conditional revalidation, for real

The last check is the one that saves bandwidth. [example.com](http://example.com)'s page is 577 bytes. Send its `Last-Modified` date back as `If-Modified-Since`:

```bash
curl -s -o /dev/null -w "HTTP %{http_code} | %{size_download} bytes\n" \
  -H "If-Modified-Since: Thu, 05 Nov 2026 21:17:41 GMT" https://example.com
```

Result: `HTTP 304 | 0 bytes downloaded`. Nothing changed since 5 November, so the server sent nothing at all — no body, just the status. That's the entire point of caching headers: the second visit costs zero bytes when nothing changed, and the first visit after a change costs the full page. The 304 is the web's cheapest response.

## What to actually remember

- **Cache-Control** is the instruction sheet: `public` vs `private` (who may store it), `max-age` (how long it's fresh), `no-cache` (store it, but ask first), `must-revalidate` (never serve it stale).
- **ETag / Last-Modified** are the fingerprints that make revalidation cheap: send one back, get a 304 instead of a full page.
- **Age** tells you how long the copy you're looking at has been sitting in a cache.
- **Vary** tells you the same URL can have different versions — logged in vs logged out, compressed vs not.

Next time a page loads slowly, open the network tab and read these four headers before you blame the server. The answer is usually written right there.

---

*P.S. Everything I write stays free — that part doesn't change. But if this one was worth something to you, you can buy me a coffee ☕ (one-time or monthly, both work). It covers the API credits and hosting behind these experiments — and honestly, it's also the nudge that gets the next one started on the evenings I'd otherwise skip it. — Salam*

[![Buy me a coffee](https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/87bycvv0drlpbwxjppyi.png)](https://www.patreon.com/c/AheadKit)
