Sitemap

The Click That Never Happened

2 min readOct 3, 2026

--

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=123

Simple. 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 → 42

Chrome (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:

  1. Chrome prerenders http://localhost:3000/click?id=123 → request #1 (the ghost)
  2. 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

--

--