The Click That Never Happened
How a “perfectly working” link tracker started double-counting, and the one header that fixed it.
The use case
I was building a small tracking feature: capture the open count and click count of a link. Every time someone clicks a tracked link, the server hits an endpoint, bumps a counter, and redirects.
http://localhost:3000/click?id=123Simple. Or so I thought.
Everything was fine… at first
Postman: one request, one increment.
GET http://localhost:3000/click?id=123→ 200 OK
→ click_count: 41 → 42Chrome (early testing): click the link, count goes up by one.
I shipped it to testing feeling good.
Then the numbers got weird
Later, I noticed the counts were higher than the number of clicks. I’d click once, and the counter would jump by 2.
Expected after 1 click: click_count = 42
Actual in Chrome: click_count = 43 ❌Server logs told the story:
[12:01:03] GET /click?id=123 → count = 42
[12:01:03] GET /click?id=123 → count = 43 ← who sent this??Two requests. One click. Postman never did this. Early Chrome never did this. What changed?
Get SRUJAN PJ’s stories in your inbox
Join Medium for free to get updates from this writer.
I checked for double event listeners, React strict mode double-invokes, retries, and redirects. None of them were the cause.
The culprit: Chrome’s prerender
Chrome has a speculative loading feature. When it predicts you’re about to visit a page (from the address bar, from links you hover over, or from <script type="speculationrules"> hints), it can prerender that page in the background. That means it fully loads it, including sending the real request to your server, before you've even clicked.
So, from my server’s point of view:
- Chrome prerenders
http://localhost:3000/click?id=123→ request #1 (the ghost) - The user clicks the link → request #2 (the real one)
Both look like normal GET requests. My counter couldn’t tell them apart. Hence the inflated clicks.
That also explains why it appeared “later.” Chrome’s predictor learns from your behavior, so it only started prerendering after I’d visited that URL a few times.
The flow
User hovers / types link
│
▼
Chrome prerenders /click?id=123 ← speculative, user hasn't clicked yet
│
▼
GET /click?id=123
Sec-Purpose: prefetch;prerender
│
▼
Does Sec-Purpose contain "prefetch"/"prerender"?
├── YES → serve response, DON'T count
└── NO → real visit → count++The fix: look at the header
Chrome is honest about what it’s doing. Speculative requests carry a header:
Sec-Purpose: prefetch;prerender(Plain prefetches send Sec-Purpose: prefetch. Older Chrome versions also used Purpose: prefetch.)
So the fix is: if that header is present, don’t count it.
References
- Chrome for Developers: Prerender pages in Chrome for instant page navigations — https://developer.chrome.com/docs/web-platform/prerender-pages
- Chrome for Developers: Speculative loading / Speculation Rules — https://developer.chrome.com/docs/web-platform/implementing-speculation-rules
- MDN: Speculation Rules API — https://developer.mozilla.org/en-US/docs/Web/API/Speculation_Rules_API
- MDN:
Document.prerenderingand theprerenderingchangeevent — https://developer.mozilla.org/en-US/docs/Web/API/Document/prerendering - MDN:
Sec-Purposeheader — https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Sec-Purpose







