I told readers to date a Vercel deploy by reading two headers, and both of them moved without a deploy
On 31 October I published a post about the three caches sitting in front of PubTrivia's 72 public pages, and I ended one section with a tip: if you want to know when a Vercel site last shipped and you cannot see its dash
On 31 October I published a post about the three caches sitting in front of PubTrivia's 72 public pages, and I ended one section with a tip: if you want to know when a Vercel site last shipped and you cannot see its dashboard, read the age header. Every prerendered page was reporting about 1,052,000 seconds, which is 12.2 days, which lined up neatly with our last deploy.
I re-ran the identical sweep this morning. Nothing has shipped since 18 October. Here is what the same page says now:
$ curl -sI https://pub-trivia.app/features | grep -iE '^(age|date|etag|x-vercel-cache)'
age: 213799
date: Sun, 01 Nov 2026 23:23:52 GMT
etag: "qatmf2if0t1r53"
x-vercel-cache: HIT
213,799 seconds is 2.5 days. The header that was supposed to be a deployment log now points at a Sunday evening three weeks after the build. The trick was wrong, and chasing down why turned up something more useful than the trick was.
Nothing shipped, so what moved
First, the deploy really is the old one. git log ends at 18 October. Every asset URL on every page still carries the same ?dpl=dpl_EKzaTAmQUBUdseoodG43waJqVSuS deployment id I have quoted in three earlier posts, and 70 of the 72 sitemap URLs still answer with x-nextjs-prerender: 1. Same build, same output, same two per-request exceptions (the homepage and the pricing page, both of which read the visitor's country to pick a currency).
What moved is the cache object. Across the 70 pages that answer HIT, there are 62 distinct ages, running from 79,333 seconds on one question page to 226,655 on the terms page. Converted to clock time with the date header, the objects were filled between 19:36 on 1 November and 12:31 on 3 November, most of them in a single evening window, page by page.
That also kills the arithmetic I published. date on a cached response is the moment the origin generated it, and age is how long ago that was, so date plus age is simply now. They are one fact wearing two hats, and the fact is when this edge node last pulled the page, not when the page was built. An object that gets evicted and refetched resets both numbers while the build sits still.
The ETag I printed in that post is already dead
If the build has not changed, a prerendered page should be the same bytes it was last week, and the validator should still match. It does not:
# The ETag I published for /features on 31 October
$ curl -s -o /dev/null -w '%{http_code} %{size_download}\n' \
-H 'If-None-Match: "jkosq8e831r6q"' https://pub-trivia.app/features
200 81893
# The one it has today
$ curl -s -o /dev/null -w '%{http_code} %{size_download}\n' \
-H 'If-None-Match: "qatmf2if0t1r53"' https://pub-trivia.app/features
304 0
81,893 bytes of prose that has not been edited since September came back down the wire, because the validator for it changed while the content did not.
Two bytes in the head make every render unique
The two per-request pages are a convenient laboratory for this, because they re-render on every request. Fetch the homepage twice, two seconds apart, and compare:
$ curl -s https://pub-trivia.app/ -o r1.html
$ curl -s https://pub-trivia.app/ -o r2.html
$ ls -l r1.html r2.html | awk '{print $5}'
150474
150476
$ cmp r1.html r2.html
r1.html r2.html differ: char 5670, line 1
Byte 5,669 is inside the head, in a <meta name="sentry-trace"> tag. Our error reporting injects a distributed tracing id into the document, and that id is new for every render. Nothing else on the page differs. Two renders of a page whose content is frozen in a build artifact are therefore never byte-identical, and an ETag is a hash of bytes.
So the chain is: the edge evicts an object after a few days, Vercel re-renders the page, the trace id is fresh, the hash is new, and every returning reader who still holds last week's ETag gets 82 KB instead of a 304. The content has not changed since September. Only the receipt has.
This is not a bug in anybody's cache. It is what happens when a per-request value lands in output that is otherwise static, and it is worth knowing before you build anything on top of an ETag you did not generate yourself.
The cache key, which is what I should have measured
While I had the sweep open I measured the thing that actually matters for marketing links: whether a query string makes a new cache object. It does not.
$ curl -sI 'https://pub-trivia.app/faq' | grep -iE '^(age|etag|x-vercel-cache|content-length)'
age: 218683
etag: "9zhhcej9bt1o63"
x-vercel-cache: HIT
content-length: 77981
$ curl -sI 'https://pub-trivia.app/faq?utm_source=dev.to&utm_campaign=x&ref=abc' \
| grep -iE '^(age|etag|x-vercel-cache|content-length)'
age: 218683
etag: "9zhhcej9bt1o63"
x-vercel-cache: HIT
content-length: 77981
Same age, same ETag, same byte count, same object. I repeated it on the features index, a guide, a comparison page and a free tool with random parameters each time: 70 of 70 prerendered pages ignore the query string entirely. The page also never echoes it, so rel="canonical" and og:url stay clean on a tagged link, which is the behaviour you want from a campaign URL.
The exceptions are the two pages a campaign is most likely to point at. Pricing and the homepage are private, no-cache, no-store and answer MISS with age: 0 whether or not you tag them, so every ad click renders them again. That is the price of showing a visitor a price in their own currency, and it is a deliberate trade, but it does mean the two pages carrying our conversion are the two with no edge cache to warm.
One more thing the sweep turned up: a trailing slash is a 308, and it keeps the query.
$ curl -sI 'https://pub-trivia.app/faq/?utm_source=devto' | grep -iE '^(HTTP|location)'
HTTP/2 308
location: /faq?utm_source=devto
Check it yourself
Everything above is one curl -sI away. Pick any URL from our sitemap, read its date and age, add them together and see that you get the current time, then compare two pages and watch the fill times disagree by hours. Tag the URL with anything you like and watch the ETag refuse to change. If you want to see the per-render id instead, fetch the homepage twice and cmp the files, because it is the one place on the site where the same request twice gives you two different responses.
And if you read the earlier post and wrote down the header trick: subtract nothing. Read date, know that it is when that edge node last pulled the page, and get the build date from the deployment id in the asset URLs instead, which is the one thing on the page that only changes when something actually ships.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.