Your packet-loss number is a property of your congestion controller
"We see 4% packet loss" sounds like a fact about the network. Usually it is a fact about your own stack. We built a link configured to lose nothing, and then made it lose half a percent — by changing the sender.
On this page
TL;DR: "We see 4% packet loss" is almost never a fact about the network. It is a fact about how hard your sender pushes, how deep the queue in front of it is, and which congestion controller is deciding. We shaped a link to discard nothing — a rate, a delay, a queue, and no loss clause at all — and then measured 0.55% loss across it. Every discarded packet was put into a full queue by the sender: self-inflicted packet loss, on a link that was told to lose none. Change the queue depth and the same link's loss rate moves more than twenty-fold.
There is a sentence that turns up in every incident review of a system that talks to the field, and it always arrives with the authority of a measurement: we're seeing about four percent packet loss.
It sounds like weather. Something the network is doing to you, which you route around or complain about but do not own. It is worth being precise about why that reading is usually wrong, because the same number, taken as a property of your own stack, points at things you can actually change.
The link that was told to lose nothing
I wanted the cleanest possible version of the claim, so I built a link that cannot fail on its own.
Two network namespaces joined by a veth pair, with tc netem on the sender's side carrying three
things and no more:
tc qdisc replace dev veth0 root netem \
rate 10mbit delay 40ms limit 10A rate, a propagation delay, and a queue of a fixed number of packets. What it does not carry is
a loss clause. There is no random discard, no corruption, no reordering. If nothing arrives at a
full queue, nothing is ever thrown away.
Then I pushed six megabytes across it with a plain TCP socket, three times per configuration, and read the queue's own discard counter.
| Controller | Queue | Goodput | Discards | Loss rate |
|---|---|---|---|---|
| CUBIC | 10 packets | 4.51 Mbit/s | 17 | 0.39% |
| Reno | 10 packets | 3.88 Mbit/s | 24 | 0.55% |
| CUBIC | 30 packets | 9.41 Mbit/s | 3 | 0.07% |
| Reno | 30 packets | 9.27 Mbit/s | 3 | 0.07% |
| CUBIC | 100 packets | 9.40 Mbit/s | 1 | 0.02% |
| Reno | 100 packets | 9.40 Mbit/s | 1 | 0.02% |
Read the last column on its own first. The same link, with the same settings and no loss configured, reports anywhere between 0.02% and 0.55% packet loss — twenty-four discarded packets against one, on identical settings. The network's own reliability was constant throughout, because it was perfect throughout.
- Reno
- CUBIC
Why does a link that drops nothing drop things?
Because "drops nothing" is a statement about the link's failure modes, not about its capacity.
A queue is not a promise. It is a fixed amount of somewhere-to-put-things while the thing in front drains at its own pace. A sender that hands over data faster than the drain rate fills it, and the moment it is full the next packet has nowhere to go. That discard is not a failure of the link. It is the link telling the sender, in the only language it has, that the sender is wrong about how fast it may send.
Which is the entire design of loss-based congestion control. TCP does not have a channel for "please slow down"; it infers the send rate by pushing until something is lost and backing off. So a loss-based sender on a healthy path does not encounter loss so much as manufacture it, on purpose, as its measurement instrument. The number you are reading off your dashboard is that instrument's output.
The queue is doing more of the work than the network
The most striking pair in the table is not the two controllers. It is the two queue depths.
At a hundred-packet queue, both controllers move 9.4 Mbit/s and discard one packet in the whole transfer. At ten packets, the same controllers on the same link get less than half the goodput and discard seventeen to twenty-four times as much. Nothing about the link's speed changed. What changed is how much slack the sender had to be wrong with.
That is worth carrying into any conversation about a "bad" connection, because the queue is usually somewhere you cannot see and did not choose: a modem, a base station, a virtual NIC, a container runtime, a cloud load balancer. When two deployments of the same software report different loss rates, the buffer in front of them is a better first suspect than the radio.
The reverse case is the one with a name. Make the queue very deep and the discards go to zero — but the packets are not moving faster, they are waiting longer. The sender fills the buffer, the round-trip time inflates to the depth of it, and everything that cares about latency degrades while every loss metric reads perfectly healthy. That is bufferbloat, and it is the reason "zero packet loss" is not the reassurance it sounds like.
Two controllers, one link, different answers
Now the controllers, which is the part the title is about.
At the shallow queue, Reno discarded 24 packets to CUBIC's 17 — about 40% more loss on an identical link — and got about 14% less throughput for it. At the deeper queues the two are indistinguishable, because neither is ever pushed into the regime where the difference expresses itself.
Both of these are loss-based controllers, which makes them close relatives; the spread is modest precisely because they are answering the same question with the same instrument. Both push until something is lost. Comparisons across families — a loss-based controller against one that models the path instead — are much wider, and they are the reason the effect is worth naming rather than filing under trivia.
The largest such study I know of is a measurement campaign on Chinese high-speed rail (MobiCom '19): 1,732 GB captured over 135,719 km of trips above 300 km/h, with an explicit head-to-head of TCP CUBIC against TCP BBR on the same routes.
Those three figures describe the study's scale, they are in its abstract, and they are the reason it is worth naming rather than gesturing at. What I am not quoting is its result — the size of the spread it found between the two controllers. BBR was not available on the kernel this ran on, so I reproduced none of it, and a number I cannot point at a line of my own is a number I should not be carrying into a sentence of mine.
So the honest general statement is narrower than the headline and more useful: the loss rate an endpoint observes is a joint property of the path, the buffer in front of it, and the algorithm choosing the send rate — and only the first of those three is what people mean when they say "the network".
What to do with a loss number instead of quoting it
Four things, in the order that gets you to an answer fastest.
Say which endpoint measured it, and where. A figure from the client, the server, a middlebox and a carrier's own tooling are four different measurements. They will not agree, and the disagreement is information rather than noise.
Record the controller alongside it. ss -ti prints it per socket. A loss rate without a
controller is like a latency figure without a percentile: not wrong, just not yet a number.
Look for the queue before blaming the medium. Ask what sits between the sender and the first hop you do not control, and how deep it is. If nobody knows, that is the finding.
Watch loss and latency together, never loss alone. They trade against each other across queue depth, so either one on its own can be made to look excellent by making the other worse. A system tuned on loss alone will be tuned into bufferbloat.
A locally dropped packet is invisible to your retransmission counter
FAQ
Does this mean packet loss numbers are useless? No — it means they are relative. A loss rate is perfectly good for comparing the same system against itself over time, which is what most alerting actually needs. What it will not support is a comparison across two stacks, two controllers or two deployments, or a claim about the network in the abstract. Treat it as a reading from your instrument, not a property of the world.
We measure loss at the application layer. Does any of this apply? More so, and with an extra hazard. An application-layer counter sees losses that were not recovered plus whatever your framing calls a loss, on top of everything above. Our own run found a sharp case of the counter and the reality disagreeing: the sender's retransmission counter read zero throughout, while the queue was discarding dozens of packets. A packet dropped by the sender's own egress queue never reaches the wire, so TCP re-sends it without ever recording a retransmission. If the bottleneck is inside your host, the metric most people reach for cannot see it at all.
Is a bigger buffer the fix, then? Only if you do not care about latency, which for anything real-time you do. Deepening the queue moves the cost from loss to delay rather than removing it — that is bufferbloat, and it is the more expensive failure for a live system, because it degrades everything at once instead of degrading one recoverable thing. The actual fix is a queue sized to the bandwidth-delay product with active queue management, not a bigger one.
Should we just switch to BBR? Perhaps, but not on the strength of a loss number, which is precisely the reasoning this article exists to interrupt. BBR models the path rather than probing it with losses, so it will usually report less loss almost by construction. Whether it delivers better for your traffic is a question about your bottleneck, your competing flows and your fairness requirements — and it needs measuring on your path, with goodput and latency in the frame, not loss alone.
The three things worth taking away
- A link configured to lose nothing lost half a percent, because a loss-based sender does not discover the limit — it finds it by crossing it. Loss is the instrument, not only the symptom.
- Queue depth moved the same link's loss rate more than twenty-fold and halved the goodput, without the network changing at all. When two deployments disagree, look at the buffer before the radio.
- The controller changes the answer — 40% more discards between two close relatives here, and the literature compares whole families rather than cousins. A loss figure without the controller beside it is not yet a number.
Where a number like this actually gets read, and what it is usually blamed for, is on freight telemetry that survives a bad link.
If you are reading a loss figure and are not sure what it is a property of, that is the kind of thing we come in for.