One capture runs two kinds of probe at once. Continuous probes (ICMP, UDP, QUIC, DNS…) tick the whole time — lightweight, meant to overlap everything and catch every latency spike and outage. Scheduled probes (speed tests, video quality) are bandwidth-intensive, so they run one at a time and never contend for the link. The phone apps and the desktop tool schedule them differently; this page is generated from both schedules — every number below is what the apps actually run.
Scheduled probes per capture, one square per test. Speed tests are our own test plus the Akamai and Netflix cross-checks; video tests are our hosted stream plus YouTube. LEO covers any low-latency link (Starlink, ground Wi-Fi); GEO satellite tests take about 3× longer, so desktop fits fewer of them. The Windows app moves to this schedule in its next release.
| Capture | Phone · LEO | Phone · GEO | Desktop · LEO | Desktop · GEO |
|---|
Pick a capture length to see when each scheduled probe runs. Continuous probes run underneath the whole time and aren't drawn.
The desktop tool (macOS, Windows, CLI) groups its scheduled probes into rounds, spaced across
a 30-minute cycle by the wave scheduler (scheduler.py).
Scheduled probes split into flood (speed tests — they saturate the link, so they're kept sparse and spaced to leave the latency probes clean unloaded windows) and user interaction (video / CDN traffic — dense is fine because it mimics a real passenger and doesn't saturate). The link tier is the continuous probes running underneath everything.
Whenever a capture ends, has it collected the most useful set? Short captures drop rounds from the end, so a longer capture is a strict superset. GEO probes cost more, so the same capture length collects fewer rounds on satellite than on a fast terrestrial link.
| Capture | GEO satellite | Terrestrial / LEO |
|---|
Two independent axes. The profile (active / passive) retunes the continuous probes;
the schedule repaces the heavy scheduled rounds. The CLI drives both directly
(wifi-capture --profile / --schedule). The macOS app folds them into one
three-stop Light / Normal / Heavy control — Light is --profile passive,
Normal is the default, Heavy is --schedule intensive — available on captures of
15 minutes or longer, below which every stop runs Normal (shorter captures truncate a
single cycle, so Heavy would have nothing to add). more-video and
budget-video stay CLI-only: both are video-first and need
--allow-metered.
The iPhone and Android apps have no profile/schedule axes; Continuous captures offer a two-stop
Normal / Heavy control instead. Normal runs 7 of our speed tests, 2 Akamai and 2 Netflix
cross-checks per 30-minute cycle; Heavy adds 3 more of our speed tests to the same cycle (20 an hour,
matching desktop Heavy). Heavy pauses its extra tests while the phone is hot or in Low Power Mode,
warns on battery, asks first on cellular, and returns to Normal after 6 hours.
Duty cycle is the share of each cycle spent actually probing — the saturation number.
ST counts every throughput test, our own and Akamai both (the Akamai runs are a deliberate
CDN cross-check, not overhead).
| Intensity | Flag | Cycle | Probe time | Gap / round | Duty cycle | 3h capture | 5h capture |
|---|
Download and upload speed via four parallel HTTP transfers — it saturates the link (like Ookla) to
measure true capacity. It is our own test, not Cloudflare’s: it runs against our speed-test
server on tcp.howismywifi.net (on iPhone and Android, alternating with an HTTP/3 twin on
quic.howismywifi.com), and falls back to speed.cloudflare.com, then to Akamai
when shared satellite NAT IPs trip per-IP rate limits. From iPhone build 116, our test runs TCP only on GEO links. (The Windows app moves to these hosts in its next
release; until then it tests against our older server.) On GEO the test runs longer (~12 s) so the high-latency link has time
to reach steady state. The reported speed is the sustained rate after that ramp — the
median of each round's post-ramp steady-state throughput, not a whole-transfer average (which would read
low on high-latency links still climbing the ramp).
A second speed test on a different CDN (dash.akamaized.net). Running it alongside the
main speed test cross-validates capacity and exposes per-CDN shaping — an airline may cap one CDN
while another runs clear on the same link, which a single backend would never reveal.
Download throughput from the same Netflix Open Connect servers that serve real Netflix video, mapped to a streaming resolution tier — so it answers a question users actually have: would Netflix play in HD here? It surfaces per-CDN rate limiting directly: Netflix may be capped at 2.3 Mbps on GEO while the main speed test sees 10+ Mbps on the same link. Successive Netflix slots in a capture alternate between one connection and four to the same server, starting on a random one, which tells a per-connection video cap from a whole-device one. The Netflix number and resolution tier come from the one-connection runs; a short capture whose only Netflix run used four connections shows that value labelled "4 connections", with no resolution tier. Passive, data-capped and on-demand runs always use one connection.
yt-dlp resolves a video's DASH/HLS manifest, then downloads four real media segments and
times each — reporting achievable bitrate, time-to-first-byte, CDN host, codec, and a recommended
resolution. Fast and dependency-free, it runs on every YouTube slot (no Chromium needed) and is the data
behind the "Recommended quality" chip on the results page.
When Chromium is installed, this launches a real headless YouTube player in two phases: AUTO (let YouTube's own algorithm pick the quality on this link) and FORCED (ask for the resolution the segment probe said should work, and see what actually happens). It captures real startup latency no throughput test can — and when the segment probe says 1080p but ABR locks to 480p, the platform is throttling the player itself.
Drives a real headless Shorts feed — swiping through short-form clips and reading comments — to measure the rebuffering and quality a passenger sees on the format they actually use most. Two flavors: a standard ~3-min session, and a Deep ~6-min round ( on GEO; varying swipe cadence, comments read twice) that stress-tests sustained short-form playback. The stall-based quality verdict is withheld until it's validated on a real flight.
Plays a real adaptive video stream from our own server (video.howismywifi.net): HLS in
2-second segments, five H.264 renditions from 240p at 300 kbps to 1080p at 5 Mbps (a bitrate-matched VP9
set on desktop). Startup is timed on our own clock, from pressing play to playback actually moving (phones show a
first frame earlier, then wait for enough video), and split into its round trips (DNS, connect, TLS, the two playlists, the init and first segments, then the
first frame), so a slow GEO start reads as so many round trips × RTT rather than one opaque number. It also
records stalls, buffer ahead, dropped frames, per-segment speed and time-to-first-byte, the share of segments
that arrived faster than real time, and whether each request came from the edge cache (a first request at a
rarely used Cloudflare site reads slow). Each run opens with a forced phase: a cold start on a single
rendition that our server sets (1080p / 5 Mbps), identical on every device, so GEO, LEO and terrestrial
compare like with like. The clip comes from a short curated pack our server publishes. The app rotates
through it, the two phases of a run play different clips, and every row records which clip played. Each
clip is encoded to the same bitrate targets before it joins the pack, so clip-to-clip differences stay
within a tested bound, and results are audited per clip. The forced phase runs 20 s
and the ABR phase 24 s. The server can tune both within fixed limits, and results pool only
across runs at the same forced quality and length. An
ABR phase, where the player picks the quality (capped at 1080p), follows on desktop in the
full-video and Deep rounds, right beside the YouTube player test so the two compare on the same link; desktop
also runs a forced phase alone in its first round and in Var (4 forced and 2 ABR phases per 30 minutes, the
first about 1–2 minutes in). The phone apps run one forced phase in Lightning and in Standard (first, as soon as the app can show it),
and one after each of the 7 speed tests in a 30-minute Extended or Continuous cycle, one of
them a full run with the ABR phase. On iPhone (iOS 26+, build 113+), a phase that comes due while the app is in the
background waits for the app to come back, and is skipped (skipped=backgrounded) only once it runs out of time, so a capture that runs in a pocket records no video. The ABR phase is reported per platform
only, because iPhone, Android and desktop players adapt differently. It also runs when the forced phase failed
to start (no picture in time, or a playlist or player error), so its start rate is what a passenger pressing
play would see. It is still skipped on a metered connection, after a codec failure or a phase that hit its data
cap, and when video is switched off. Older app builds ran it only after a forced phase that didn't fail, so
their ABR results lean optimistic on a bad link; rows from builds that always try it carry
+aalways in player_version.
Live on iPhone (build 109+; the round-trip breakdown needs iOS 18+), Mac (0.8.12+) and Android (0.1.16+); Windows from its next release. The curated pack and the 1080p forced phase arrive with the command-line tool 0.1.1+. The player buffers ahead the way a real streaming app does, rather than being held back to save data, so on a fast link the forced phase fetches up to about 37.5 MB (the whole clip) and a full desktop run about 75 MB. Each phase is hard-capped at 45 MB. Metered links run only the forced phase. Our server isn't a known streaming service, so rules that target streaming services don't single it out: this measures what the link can do for video, and it can read better than Netflix or YouTube would on a link that throttles them. It's a measurement, not a quality score. Videos: An Ocean in Motion and Perpetual Ocean, NASA Goddard Scientific Visualization Studio (public domain); Thermonuclear Art: The Sun in UHD, NASA Goddard / SDO (public domain); Sol Levante, Netflix with Production I.G | opencontent.netflix.com, licensed CC BY 4.0.
Base round-trip time and packet loss — the most fundamental measurement, an echo request/reply with no application-layer overhead. A gateway ping runs alongside to separate a local Wi-Fi problem from an internet-path one, and two internet targets guard against one being rate-limited.
A real UDP round-trip via a minimal DNS query — a second latency and loss signal independent of ICMP. PEPs don't repair UDP loss, so this stays honest on GEO where TCP and ICMP loss are masked. UDP loss, RTT, and jitter feed the Voice MOS estimate.
Times a TCP three-way handshake to port 443. This is the key detector for TCP Performance Enhancing Proxies: airlines deploy transparent proxies that answer with a local SYN-ACK, making TCP look far faster than the real satellite round-trip. A large ICMP-vs-TCP gap is the PEP signature.
A full UDP + TLS 1.3 / HTTP-3 handshake. Because QUIC is UDP, a TCP PEP can't intercept it — so QUIC RTT reveals the true path latency the proxy hides. It's also increasingly the protocol passengers actually use: YouTube, Google, and Cloudflare all default to HTTP/3.
The iPhone and Android apps run QUIC on its own timer at the Active cadence shown here (the apps have no separate Passive/background profile). On a degraded satellite link some UDP:443 handshakes genuinely time out, so QUIC keeps a minimum attempt floor (about one sweep every ~15s) rather than backing off — coverage thins but never stops.
Full-stack latency — DNS + TCP + TLS + request/response — which is what you actually feel loading a page. Canary targets and response-header inspection also expose intercepting proxies along the path.
End-to-end resolution time as applications experience it, via the system resolver — so it includes real OS DNS-cache effects rather than a synthetic lookup.
The full network path — every router hop with per-hop latency. Path changes correlate with performance changes; on satellite a path change can flag a beam handover or gateway switch, and the hop count itself is informative (satellite links show few hops).
RF and PHY-layer metrics from the adapter — RSSI, noise floor, SNR, channel, width, band, PHY mode, TX rate. Correlating signal with performance separates RF problems from network problems: a latency spike with an RSSI drop is local; one without points upstream.
Passively reads the network interface's byte counters to track how much real traffic is flowing over the link. It sends nothing itself — it's context for how loaded the link was when each active probe ran.
Each round's duration is the sum of its probe durations for the selected link. The heaviest round is tagged on GEO (the binding link).
| Round | Probes | Duration |
|---|
Drop-from-end trimming: a capture keeps as many leading rounds as fit its budget (after the one-time latency preflight), then stops.
| Capture | GEO | Terrestrial / LEO |
|---|