Every guide about disappearing pop traffic stops at the same place: the visitor never reached your page. That is real, and it is measurable. But it is only half the story, and the half nobody publishes is the one that will actually change what you do on Monday.
We instrumented a single live pop campaign end to end for a 7.5-hour window — the ad network's own billing counter, the CDN edge, the origin server, the filter decision, the HTTP response, and then three separate client-side signals. Here is the ladder, in unique visitors, for one campaign in one aligned UTC window:
| Stage | Count | Survives |
|---|---|---|
| Network billed us | 395 | — |
| Reached the CDN edge | 218 | 55.2% |
| Reached our server | 206 requests / 176 unique | 94.5% of edge |
| Passed the filter (accepted) | 169 | 96.0% of unique |
| Page actually served (HTTP 200) | 154 | 91.1% of accepted |
| HTML parsed (no-JS signal) | 118 | 76.6% of served |
| JavaScript executed | 91 | 77.1% of parsed |
| Visible for 1 second | 69 | 75.8% of JS |
| Still visible after 10 seconds | 33 | 47.8% of visible |
Read the two bolded rows again, because that is the finding.
Of the pages we successfully served, 36 were never parsed and 27 were parsed but never ran JavaScript
Those are two completely different failures that every analytics setup in the world reports identically: as nothing.
- Served but never parsed (36). Your server returned 200. The bytes went out. The browser never got far enough to build the document — the tab was closed, backgrounded into oblivion, or killed before parsing completed. Your server log says "success". Your analytics says nothing happened.
- Parsed but JavaScript never executed (27). The document was built. The HTML was read. And then the JavaScript simply did not run — a suspended background tab, an aggressive power-saving mode, a webview that froze the timer queue, or a blocker that killed the script.
If you only measure with a JavaScript tag — which is what analytics, tag managers and almost every tracker are — these 63 visitors are one undifferentiated blob of "missing". You will conclude you bought 63 bots. You did not necessarily buy 63 bots. You bought some number of real humans whose browser never gave your script a turn.
How to separate them (the instrument nobody installs)
You need one signal that fires without JavaScript. That is it. That is the whole trick.
Put a plain <img> tag in your HTML body pointing at an endpoint you control, with the identifier of the click in the query string. No script. No onload. Just an image request the HTML parser issues on its own as it reads the document.
Now you have three tiers instead of one:
- Server 200 — you sent the page.
- Image request arrives — the HTML was genuinely parsed by something that reads HTML.
- JavaScript beacon arrives — a script engine actually ran.
The gap between 1 and 2 is delivery that never became a document. The gap between 2 and 3 is a document whose scripts never executed. Those two gaps have different causes and completely different fixes, and until you split them you are guessing.
One caution from our own build: we first tried to measure "HTML parsed" using a signal that was itself emitted by JavaScript. It returned exactly the same number as our JavaScript-booted count — 91 and 91 — which looked like a beautiful clean funnel and was in fact a tautology. If your parse metric always equals your JS metric, your parse metric is JavaScript. Throw it out.
Before the page: the loss you cannot fix by touching your site
In this window the network billed 395 and only 218 ever reached the CDN edge. That is 44.8% that produced no DNS lookup, no TCP connection, no TLS handshake, no request line anywhere — measured at the edge, not inferred from an empty log.
This matters because it is the number people most often misdiagnose. It is not a rendering problem. It is not a bot-filter problem. It is not your landing page being slow. Nothing of yours was ever contacted. On pop inventory the impression is typically counted when the pop opens at the publisher, which is upstream of your destination finishing — or starting — its navigation.
So keep these three buckets rigidly separate, because they need different responses:
| Bucket | What it means | What actually helps |
|---|---|---|
| Billed, never reached your edge | No request was ever made | Source/zone selection. Nothing on your server can recover it. |
| Reached you, page served, never parsed | Tab died before the document existed | Faster first byte, smaller HTML, no redirect chain. |
| Parsed, but scripts never ran | Background/suspended/blocked execution | Server-side measurement. Stop depending on JS to count. |
Lumping all three together under one word — "ghost", "no-render", "bots" — is why this problem has stayed unsolved for so many buyers for so long.
The counting traps that will lie to you
Every one of these produced a wrong number in our own build before we caught it. They will produce wrong numbers in yours.
1. Requests are not visitors
One visitor in our window loaded the article 12 times — a re-navigation loop — and generated 5 filter decisions inside the measurement window alone. If you count raw log lines, that person is 12 visitors. Deduplicate on the click identifier before you divide anything, or your accept rate will exceed 100% and you will not know why.
2. Never divide two different windows
If your network's reporting covers 05:55–13:30 and your server log covers midnight to now, dividing one by the other is meaningless. We produced an "825% arrival rate" this way. The fix is not a smarter formula — it is refusing to compute the ratio until both sides cover the identical interval, and printing "N/A" until they do.
3. Never divide two different populations
Your CDN sees every route on the domain. Your campaign is one route. Putting a campaign-scoped numerator over a site-wide denominator produces a number that looks precise and means nothing. Scope both sides to the same campaign, or label the row honestly as site-wide.
4. Long-dwell signals are not quality signals
Our 10-second signal fires only while the tab is visible. Of 69 visitors who were genuinely visible for a full second, only 33 were still there at ten. That is not 36 bots. That is pop traffic behaving like pop traffic. Use a long-dwell metric as a secondary read, never as your primary quality KPI, or you will blacklist your way to zero volume.
5. Zero is not the same as unknown
If a layer cannot be measured for a window, it must read N/A with the reason, never 0. A zero silently becomes a denominator somewhere downstream and quietly poisons every conclusion built on top of it.
Render rate is not a quality score — in either direction
The trap that costs the most money is assuming higher render is always better.
One zone in our data rendered at 99.3% — near-perfect on every client-side signal, the kind of number that makes you want to scale it. It was also 170 visits from 169 different IP addresses, more than 90% of them inside a single hour, presenting an almost uniform device profile. We sampled 20 of those addresses against an independent proxy-intelligence service: 19 came back as residential proxy exits.
They rendered beautifully because they were real browsers on real home connections. They were also not an audience. Meanwhile the browser-family split across normal supply in the same period looked like this:
| Browser family | Reached JavaScript execution |
|---|---|
| iOS Safari | 81% |
| Desktop Chrome-family | ~58% |
| Android Chrome | 57% |
| One major Android OEM browser | 44% |
A zone that is 90% Android in a background tab will look "bad" and a zone that is 90% rotating residential proxy will look "excellent". If your blocklist is driven by render rate alone, it is actively selecting for the second one. Render tells you whether you could measure the visitor. It does not tell you whether the visitor was worth having.
The audit you can run this week
You do not need our stack to do this. You need five things joined on one identifier, over one identical time window:
- The network's own count for the campaign. If their API only exposes lifetime totals, snapshot it every few minutes and difference consecutive snapshots — and if a lifetime counter ever goes down, treat it as a restatement on their side and contribute zero, never negative traffic.
- Your CDN's edge count, filtered to real visitor requests only. Exclude your own worker subrequests and prefetch machinery — we found hundreds of edge rows that were internal prefetch traffic and had never been near a human.
- Your origin log, deduplicated to unique clicks, with the status code kept.
- A no-JavaScript image beacon in the HTML body.
- Your JavaScript beacon, with a visibility check.
Then enforce one rule on yourself, which is the rule that made our own numbers trustworthy: every unique click must land in exactly one bucket at every stage, and no child stage may exceed its parent. Arrived equals accepted plus blocked plus not-decided plus unclassified. If that equation does not balance, you have a measurement bug — not a traffic insight. Fix the arithmetic before you draw a single conclusion, or you will spend months optimising against your own instrumentation errors.
What the honest conclusion looks like
For the campaign above: 44.8% never reached us at all, 5.5% was lost between edge and origin, the filter accounted for every single arriving click with none unexplained, and then the largest recoverable loss was entirely post-delivery — 36 pages served but never parsed, 27 parsed but never executing script.
That last number is the one worth your attention, because it is the only one on the list that is simultaneously large, real, and not about buying different traffic. It is about no longer depending on a script engine to prove a human was there.
And the honest limit: we have not yet run a full closed 24-hour window on this campaign, so we are not claiming a trend. Anyone showing you a quality trend built from partial windows is showing you noise with a confident caption.