I thought there was nothing left on this machine I hadn't measured. I counted the five different answers the disk gives about free space, chased down what keeps it from sleeping, learned how many backups I have and discovered that none of them belong to this machine. I kept skipping the line itself, because I already knew one number about it and that number was good: fast.
Today a second number joined the first. My line really is fast — the download figure came out at 480 Mbit/s. Inside that same measurement, my latency climbed from 44 ms idle to 291 ms — those are the rounded figures; the exact ones are in the table below. So the part of my line I brag about is accurate, and the part I use all day is not.
The tool behind these numbers, and why that one
macOS ships a command called networkQuality, written by Apple. Apple introduced it with iOS 15 and macOS Monterey; the WWDC22 session describes using it to measure buffer bloat both in a service provider's network and on your own server. It reports the result as round-trips per minute — RPM — rather than milliseconds.
I picked it for one reason: what it measures is defined in an IETF draft, and Apple engineers are among that draft's authors. draft-ietf-ippm-responsiveness-09 is dated 6 July 2026 and is an active draft heading for Standards Track. The definition of RPM is a single line there: you divide 60,000 by the round-trip time in milliseconds. The draft even admits the abbreviation winks at engine revolutions.
The draft's real claim, though, is a measurement philosophy. It argues that idle latency is a poor predictor of how a network behaves while data is actually flowing, so it first fills the bottleneck to capacity and measures latency after that. The difference between timing a car on an empty road and timing one in traffic.
The draft sorts scores into four categories and gives the boundaries in RPM and milliseconds together: below 300 RPM (200 ms) is "poor", 300–1,000 is "fair", 1,000–6,000 is "good", above 6,000 RPM (10 ms) is "excellent". I went in holding all four in mind; which band a number lands in can't be stated without knowing how many bands there are.
Four runs, 4 November 2026, 18:13–18:16
I made it a rule a while ago not to publish off a single run, because a single run tells the story of that one moment. I took four runs. For the first I read the link state with a separate command; for the next three I had the run script print it — why that distinction matters comes up at the end. The machine is a MacBook Pro, macOS 26.6.2 (25G83); the measurement server was Apple's Frankfurt edge (defra1-edge-fx-*.aaplimg.com).
| Run | RPM | Base RTT | Download | Upload | Duration | Idle h2 (mean of 8) | Under load, h2 (my own flow, median) |
|---|---|---|---|---|---|---|---|
| 1 | 205.9 | 47.0 ms | 474.3 Mbit/s | 50.7 Mbit/s | 37.9 s | 44.1 ms | 337.0 ms |
| 2 | 258.9 | 52.1 ms | 418.9 Mbit/s | 60.0 Mbit/s | 13.3 s | 43.3 ms | 290.0 ms |
| 3 | 162.8 | 48.9 ms | 485.1 Mbit/s | 50.3 Mbit/s | 23.3 s | 46.1 ms | 291.5 ms |
| 4 | 295.0 | 51.2 ms | 512.9 Mbit/s | 52.2 Mbit/s | 14.8 s | 43.0 ms | 268.0 ms |
| median | 232.4 | 50.0 ms | 479.7 Mbit/s | 51.4 Mbit/s | 19.1 s | 43.7 ms | 290.8 ms |
All four runs were taken while the machine was working: the load averages I recorded sat between 6.26 and 7.36. I didn't measure on an idle machine, because my machine isn't idle during the day.
Put the two columns side by side: 43.7 ms idle, 290.8 ms under load. Note the asymmetry: the left is the mean of eight measurements, the right the median of hundreds — the tool collects them differently and I left it that way. The ratio is 6.66. My line doesn't get six and a half times slower when load arrives — it takes six and a half times longer to answer, which is exactly what I feel during the day.
The median RPM is 232.4. By the draft's own boundary that sits on the "poor" side — below 300. All four runs landed under that boundary; the best one crept up to 295.0, the worst dropped to 162.8. So this isn't an outlier, it's my line's character.
Convert the idle 43.7 ms into RPM and you get 1,374 — the bottom end of the draft's "good" band. So idle, my line sits in a good band; under load it drops to the lowest one, "poor": two bands at once. That gap is the distance between the number I brag about and the number I live with.
Let's not skip one detail: the output contains two numbers that could both be read as "idle", and they are not the same thing. The tool's own base RTT has a median of 50.0 ms, while the idle HTTP request-response time is 43.7 ms. The interesting part is the direction: the larger of the two is the one meant to measure a round trip of the path, not the completion of a request. The naive reading expects the opposite. Since the tool doesn't document when or against what it computes base_rtt, I can't explain the gap; I'm only noting it. Deciding which one to quote is already impossible to avoid, even inside a single output.
While I'm at it, let me name the weak spot in my own measurement. The under-load number comes from 271–607 probes depending on the run (its twin on fresh connections, 268–714); the idle number comes from only 8 in every run. Keeping the idle sweep short is Apple's implementation choice; the draft prescribes no count. But the result stands: the two columns in that table do not carry the same statistical weight. It's more honest to read 43.7 ms as "the mean of eight measurements" than as "my line's idle latency".
Where the queue builds
The mechanism is clear in the frame RFC 9330 sets up when it describes the L4S architecture: the root cause of queuing delay is not the queue itself but the capacity-seeking congestion controllers of senders. The sender tries to fill the link, the buffer at the bottleneck swells, and my small request lines up behind that swollen buffer.
The detail of the measurement supports that picture. The tool runs two separate probes: one over the connections carrying the load, one over connections opened fresh each time. On the load-bearing connections the median is 290.8 ms; on the freshly opened ones, 143.8 ms. Both sit far above the idle 43.7 ms, but the connection shouldering my own load waits twice as long.
The tail of the distribution teaches more. In the first run, of 607 probes under load the p99 was 4,046 ms and the worst was 6,617 ms. Six and a half seconds. Averages behave politely; tails do not.
Which direction the bottleneck is in
Two numbers sitting next to each other in that table give a strong hint about where the queue forms: the download median is 479.7 Mbit/s, the upload median 51.4 Mbit/s. A ratio of 9.33 to 1. My line goes in through a wide door and out through a narrow one.
The tool's method leans on that asymmetry. According to the output, the download phase opens 9–15 parallel flows and the upload phase 10–13, and the two phases run in the same window — in every run the start and end timestamps of the upload and download phases match exactly. So during the measurement both directions of the line are being filled at once.
The thin direction is upward. When you fill 51 Mbit/s with a dozen flows, the bottleneck buffer swells in the upward direction, and even though the answer to my small request comes from below, the request itself and the TCP acknowledgements wait behind the queue above. The inference here is mine: the measurement doesn't give me the directions separately, only overall responsiveness while both are being filled. I did not fill the upstream alone and measure latency, so I'm not saying "the queue is upstream" — I'm saying "asymmetry plus simultaneous filling makes a queue in the thin direction likely". I care about the distinction so the measured part and the derived part don't end up in the same bucket.
Could I recompute the number myself
The draft gives the formula, so I tried to rebuild the tool's number from the raw samples. The formula is this: one RPM from the mean of the TCP, TLS and HTTP times on foreign connections, another RPM from the HTTP time on the load-bearing connection, and the result is the average of the two.
When I ran the first run's raw arrays through an arithmetic mean I got 187.4; the tool said 205.9. A 9% deviation. The draft also explains why it doesn't match exactly: the TM in the formula is not a simple mean — it's a single-sided trimmed mean, defaulting to the 95% percentage, and it counts only the probes from the last four intervals. The JSON output doesn't mark which sample falls into which interval, so I can't isolate that subset from outside. In other words, I did not verify the number, I derived its order of magnitude; I'm writing the difference down plainly so that anyone attempting the same exercise on their own arrays knows why it won't line up.
Run duration is a measurement too
The easiest column to skip in that table is duration. Four runs took between 13.3 and 37.9 seconds — same machine, same connection, within three minutes. The tool doesn't run a fixed-length test; it fills the bottleneck, waits for the measurement to settle, and stops when it does.
That makes duration a second piece of information standing next to the result. My longest run (37.9 s) also produced the highest median under load (337.0 ms); the shortest run (13.3 s) came out better at 290.0 ms. I won't draw a trend from four points — with that few samples, calling the measurement's noise a finding is exactly the mistake. But I can say this: if you don't record how long the run took, you'll be one column short when you ask why two runs disagreed.
The same goes for the run I had to throw away. Its upload phase lasted 968.2 seconds; the longest run in the table is 37.9 seconds. Twenty-five times longer. I should have known the result was invalid the moment I saw that number — and the 0 sitting in its responsiveness field shows the tool reached the same conclusion.
What's switched off: ECN and L4S
There's a rarely examined corner of the output. In all four runs, every connection the tool saw had ECN disabled and L4S disabled — in the first run, 722 of 722 connections. All HTTP/2, none going through a proxy.
This result isn't new to me: that ECN goes unused in practice was already visible when I went through my server's SYNs and this Mac's flags. That piece was about a door standing open with nobody knocking; the difference here is seeing the bill for that same gap in milliseconds for the first time.
That means the cure for my exact problem is not in play. RFC 9330 defines L4S as an architecture for low queuing latency and sets its target at under 1 ms of queuing on average and around 2 ms at the 99th percentile; RFC 9332 describes the network-side piece, the dual-queue coupled AQM. Their categories differ: 9330 is Informational, 9332 Experimental. On my line there's no trace of either; the bottleneck buffer still behaves, in 2026, with the habits of the 2010s.
Cloudflare's AIM scoring looks at the same place: it scores internet quality across six metrics, and one of them is "Loaded Latency" outright. It sits in the same list as download and upload speed, counted with equal weight. The idle-to-loaded gap on my line is 247.1 ms.
Two lessons from measurement day
First: the line itself isn't constant. Early in the day my Wi-Fi link was on 2.4 GHz, 802.11n, negotiated at 144 Mbit/s. By evening, while I was measuring, it was on 5 GHz channel 108, 802.11ax, drifting between 576 and 680 Mbit/s. Same room, same router, a day on which I changed nothing. Had I read the link rate once and written it into the article, the number I published might have had nothing to do with the number the tool reported.
Second, and this one is my own fault: I ran one more measurement during the day and I can't use it. In that run the tool returned 0 for responsiveness and the upload phase lasted 968.2 seconds — sixteen minutes. The 5,898,238 bytes it carried, divided by that duration, works out to 0.05 Mbit/s; the upload throughput reported in the same output was 40,496,416 bit/s. Two numbers in one file contradicting each other, because one is the realised average and the other is the tool's estimate from its best window.
I can't put that run in the table, because I don't know two things: which band I was connected to at that hour, and what the machine's load was. Neither was recorded for that hour — I could guess after the fact, but a table doesn't get filled with guesses. On top of that, I saved the output through tail -40 and lost the head of the JSON. Piping a measurement tool through a truncation is the same as truncating the measurement. So is trying to record the conditions after the fact: from the second run in the table onward I started printing the link state before each run, and I wish I'd started at the very beginning.
If you're going to check your own setup
Here's the sequence that worked for anyone wanting to repeat this:
-
Measure idle latency separately. Take the base RTT with
ping, then measure under load. Look at one number only and you'll see your line's good side. - Don't do fewer than three runs, and publish the median. My four runs spread between 162.8 and 295.0; whichever single one I picked, the story would change.
- Record the run's conditions at the same time as the run. Link rate, band, signal, the machine's load. There is no such thing as remembering it later.
-
Keep the raw output.
networkQuality -cemits JSON; save it uncropped, or you'll bin a run the way I did. -
Look at the ECN and L4S fields. If they're off, the modern mechanism for managing the bottleneck queue isn't engaged. The name to search for then is SQM: an
fq_codelorcakequeue discipline on the router. I haven't set one up, so I can't say what it buys — but that's the word to look for. -
Separating the directions needs a different tool.
networkQualityfills both directions in the same window and gives one responsiveness figure; loading download and upload separately takesiperf3(with-Rfor the reverse direction) alongside a parallelping, or a purpose-built run such asflent rrul.
I haven't done the fix yet, and I'll say that too. This article isn't a resolution story; I know the bottleneck is most likely my own router and that a device or configuration capable of queue management could shrink those 247 ms, but I won't narrate an improvement I haven't measured. If I measure it, I'll write it up separately.
What speed tells you, and what responsiveness tells you
The advertisement for an internet line always carries a speed, and the speed on mine really is high: 480 Mbit/s. The second number appears in no advertisement, because it looks bad. Yet all 480 of those megabits only get used while a large file is coming down; the 291 ms gets in front of every tab opened while that download runs, and every command typed while an upload does.
What I learned here isn't really about networks. The number we measure is usually the number that's easiest to measure — and the easiest number to measure tends to be the one that says least about the experience. Measuring a disk's size is easy; measuring how fast it fills is hard. Measuring a line's speed is easy; measuring how long it keeps you waiting under load is hard. I ended up in the same place when I was hunting what was blocking sleep and when I was counting how many times my laptop woke at night: what the machine tells you easily is not what you care about most.
If you're proud of a number, ask which number next to it has never been measured. In my case the answer was six and a half times.
Official Sources
- Responsiveness under Working Conditions (draft-ietf-ippm-responsiveness) — the RPM definition, classification thresholds and measurement method
- Full text of draft-ietf-ippm-responsiveness-09 — the formulas and the trimmed-mean definition
- RFC 9330: L4S Internet Service — Architecture — the root cause of queuing delay and target queue values
- RFC 9332: Dual-Queue Coupled AQM for L4S — the network-side queue management
-
Reduce networking delays for a more responsive app (WWDC22) — Apple's explanation of
networkQualityand RPM - Cloudflare AIM: internet quality scoring — loaded latency as one of six scored metrics
Top comments (0)