TL;DR
- A browser can't see the firewall rules behind a VPN kill switch. It only sees answers: which address a server saw, or no reply at all.
- Sampling leaves gaps, and a leak shorter than the gap between two samples can vanish completely.
- No reply is not a pass. It only proves nothing came back.
You click Quit in the VPN app, and the tab that was supposed to record the leak is now behind it. That is the basic problem with any VPN kill switch test run from a web page: to test the kill switch you have to break the tunnel on purpose, and breaking it takes you away from the tab doing the measuring.
That detail shaped the VPN kill switch test on MyIPScan. I built MyIPScan, and this post covers how a browser-based recorder works and what it cannot tell you.
What a kill switch actually is (and why a browser can't see it)
A kill switch lives below the browser. Mullvad's open-source app is a good reference because its security document spells it out. The app integrates with the system firewall: "This means WFP on Windows, PF on macOS and nftables on Linux." When it cannot build a tunnel, it enters an error state that "locks the firewall so no traffic can flow (except the always active exceptions)."
The same document separates two things people often merge. The app's "kill switch" is "the fact that the connecting, disconnecting and error states prevent leaks via firewall rules." The "Lockdown mode" setting, which the document says is "regularly misunderstood as the kill switch," only decides whether the disconnected state blocks traffic too.
None of this is visible from JavaScript. A page can only send requests and record what happened to them.
How the VPN kill switch test records a drop
The test runs in three steps, and the first two exist to give the recorder something to compare against:
- Capture your own network. Disconnect the VPN completely and record the address your ISP gives you.
- Capture the tunnel. Connect again and record the VPN's exit address.
- Force the drop. Start recording, then break the tunnel the way it breaks in real life: quit the VPN app or disable the network adapter. Run it on a desktop: a phone's background tab will likely stop sampling.
Each reading is one request to the site's own /api/ip-sample endpoint, which, in the page's words, "sees the address the request arrives from and sends it back with its AS number, provider name and country." The loop asks for a sample every 1.5 seconds, and every 0.6 seconds for twelve samples after anything changes, so transitions get finer resolution than the quiet stretches.
Every sample lands in one of four buckets:
| Outcome | Meaning | Counted as exposure? |
|---|---|---|
| No reply | Nothing came back | No |
| Tunnel address | Matches step 2 | No |
| Your own address | Matches step 1 | Yes |
| Some other address | Matches neither | No, timed separately |
Only "your own address" counts as a leak. A different VPN server after a reconnect is not your ISP, so it gets its own timer.
Below is my illustration of how such a loop can be written, not the site's code:
const samples = [];
async function takeSample() {
const t = performance.now();
try {
const res = await fetch('/api/ip-sample', {
cache: 'no-store', // never answer from cache
signal: AbortSignal.timeout(3000), // a hung request is a result too
});
const { ip } = await res.json();
samples.push({ t, ip, hidden: document.hidden });
} catch {
samples.push({ t, ip: null, hidden: document.hidden }); // "no reply"
}
}
A recorder should request with cache: 'no-store', which makes the browser fetch "from the remote server without first looking in the cache" (MDN, RequestInit); a cached answer could show the tunnel address after the tunnel is gone. It should also note document.hidden per sample, to explain later why the gaps grew.
Why the gap you ask for is not the gap you get
The loop asks for 1.5 seconds, or 0.6 seconds in fast mode. The browser decides what you actually get.
According to MDN's setTimeout page, "Firefox Desktop has a minimum timeout of 1 second for inactive tabs," and Firefox for Android has a minimum of 15 minutes for inactive tabs "and may unload them entirely." Chrome throttles hidden-tab timers too, and the MyIPScan page sums up the desktop case as browsers clamping "background timers to about one per second."
Chrome also has intensive throttling, introduced in Chrome 88, where timers are checked once per minute. MDN lists its conditions: nesting count of 5 or higher, the page invisible for more than 5 minutes, silent for more than 30 seconds, and WebRTC inactive. It only applies after 5 minutes hidden, so it is not relevant to a seconds-long test.
Put the one-second clamp next to step 3. Forcing a drop means switching to the VPN app, so the measuring tab is hidden at the moment you care about. The 0.6-second fast mode gets clamped to about 1 second while the tab is hidden, and slow requests stretch the spacing further. Each request's round-trip time also adds to the timing uncertainty.
So the page says the recorder reports "the gap it achieved rather than the gap it wanted, and says how long the tab was hidden." If the achieved gap was, say, 2.0 seconds, then any statement about leaks shorter than that is off the table.
Turning samples into a window with two bounds
For a run of consecutive "your own address" readings, there are two defensible numbers:
- Lower bound: the page defines it as "the span actually observed, from the first matching sample to the last." The leak lasted at least this long.
- Upper bound: the widest window the neighbouring samples still allow. The leak could have started just after the previous sample and ended just before the next one.
Again, my illustration, not the site's code:
function exposureBounds(samples, i, j) { // i..j = matching run
const lower = samples[j].t - samples[i].t;
const before = samples[i - 1]?.t ?? samples[i].t;
const after = samples[j + 1]?.t ?? samples[j].t;
return { lower, upper: after - before };
}
The page's own example output is "at least 3.6s, at most 4.8s". With 0.6-second fast-mode sampling on both sides of the run, 3.6 + 2 × 0.6 = 4.8, which is consistent with the page's example. A single number like "4.2 seconds" would look more precise and be less true.
What polling can't prove
Silence is not success. A "no reply" sample fits a working kill switch, but also Wi-Fi reassociating, DNS failing, or a timeout. The page puts it as: "Samples that get no reply show that traffic was not moving, not who stopped it."
Short leaks fall between samples. If the achieved gap is 1.5 seconds, a 400 ms window of unprotected traffic can sit entirely between two requests and never appear. The page says it directly: "A window shorter than that gap can be missed entirely." Zero matches means "none observed at this resolution", not "none occurred".
It sees HTTP from one tab, not the whole machine. Background apps, an OS update service, a torrent client, DNS through another resolver: any of that can leak without touching the tab's requests.
It errs toward under-reporting. If your own address changed after you captured it (the page names "a reconnected router, a mobile handover, carrier-grade NAT"), "a genuine exposure can land in the 'some other address' column instead." The error stays in the conservative direction.
One run is one run. The page: "It is one session, one device, one browser and one moment," and "It is not a test of any VPN application." Nobody should publish "Brand X has no kill switch" from a single recording.
On privacy, every sample is a request, so the server sees your address each time it answers. The page states that "Readings are not logged or stored on the server," that the recording "stays in this tab until you close it," and that to name the provider "only the AS number is looked up (RIPEstat), never your address." It also offers a switch to show full addresses instead of masked ones.
Where this leaves a browser-based test
Polling from a tab is a decent smoke test: it can catch a leak that lasts seconds and time it with error bars. It cannot certify a kill switch. For that, capture packets at the interface (Wireshark or tcpdump on the physical adapter while you kill the VPN), because that is where the firewall rules act.
If you build anything like this, keep one rule: report the resolution you achieved next to every result, and never let a missing answer count as a passing one.
Top comments (0)