A number is ground truth when somebody else can reproduce it and when it says what it measured, against which clock, under what load, across which path. Most published network numbers state none of those, which is why they disagree with each other and with what users experience. This paper is about what a measurement has to declare, and how to take one that survives being questioned.
Get iperf 2 on Google Play. Every measurement on this page can be taken with it.
Round-trip time is what you measure when you do not have a common time base. It is useful and it is not the same quantity as delay: it sums two directions that are rarely symmetric, so it cannot attribute a problem to the direction that caused it, and on a wireless link the two directions are not even scheduled by the same entity.
One-way delay needs both endpoints on one clock. In iperf2 that is --trip-times, which measures end-to-end write-to-read latency and states its own precondition plainly: the client and server clocks must be synchronised. Synchronised by what, to what error, is then part of the result. PTP on a dedicated segment will hold tens of nanoseconds; NTP over the path you are measuring will not, and worse, its error moves with the load you are trying to characterise.
Why a round trip cannot say which direction spent the time
Figure 1. One way delay needs both endpoints on a common clock, which is what --trip-times requires and states. A round trip needs no such thing, which is exactly why it cannot attribute the delay it reports to a direction.
The test to apply is proportionality. The clock error has to sit orders of magnitude below the interval being measured. Tens of nanoseconds against a microsecond-scale interframe space is fine. Milliseconds of clock error against a millisecond of queueing delay is not a measurement, it is a coin toss with a decimal point.
An idle-network latency number is a marketing number. Nobody uses an idle network, and the queueing that produces real delay only exists when something is queueing. The metric that corresponds to what a person experiences is responsiveness while the path is working, which iperf2 takes with --bounceback for the request and reply, and --working-load to put real traffic underneath it in either or both directions.
The load also has to be repeatable, or successive runs are not comparable. This is what isochronous mode is for: --isochronous=fps:mean,stdev offers a burst every frame period at a stated mean rate, and holding the deviation at zero means every period offers the same burst rather than a draw from a distribution. A load you cannot restate is a load you cannot reproduce.
And the generator has to be innocent. A test whose own server is undersized measures the server. The default public endpoints in most tools, including the ones behind the iperf2 Android client, are not sized for load testing and say so; point the test at infrastructure you control when the number is supposed to mean something.
Latency results are routinely reported without checking whether the mechanism under test was still present end to end. With L4S this is not a subtlety: the congestion signal is carried in two bits, and middleboxes remap, clear or ignore them. If ECT(1) did not arrive, or CE was never applied, then the low-latency behaviour you are measuring is not the one you think you configured, and the number is describing a different system.
So verify the marking before believing the latency. Per direction, because paths are asymmetric and the return direction is where the middlebox usually is. The iperf2 Android client makes this visible as marking-integrity badges for exactly this reason: it detects that ECT(1) survived and that CE was seen, and flags a path that stripped them. The same check belongs in any automated suite, as a precondition rather than a diagnostic.
Two smaller path declarations are worth making. The congestion control in effect, selectable with -Z, --tcp-congestion, because cubic, reno, BBR and Prague produce different delay under the same load and comparing across them without saying so is a common way to publish nonsense. And the DSCP or TOS marking, since a path that treats traffic differently by class will give a different answer for the class you did not test.
The average is the least informative statistic available about a network and the most quoted. What users notice is the tail: the worst few percent of a video call, not the mean of it. So the deliverable is a distribution. iperf2 will produce latency histograms directly with --histograms, including bin width, bin count and the confidence-interval percentiles to report, which is the difference between a number and a claim about a number.
Comparing two runs then means comparing two distributions rather than two means, and the tail is where the comparison lives. A change that improves the mean and lengthens the tail has usually made the product worse.
State the invocation. A result without the command that produced it is an anecdote, and in our own engineering documents an unmeasured cell stays visibly empty rather than being filled with something plausible. Applied to measurement, the discipline is: publish the command lines, the synchronisation method and its error, the software versions at both ends, and the ceilings that were in effect. Anything rate dependent moves when the rate moves, so the rate is part of the result and not context.
Listener
iperf --server --enhanced
Responsiveness while the path is working
iperf --client <host> --bounceback --working-load --interval 1 --time 60
request and reply latency with real traffic underneath, per interval
One-way delay against a stated clock
iperf --client <host> --udp --trip-times --histograms --isochronous=60:100m,0 --interval 1 --time 60
write-to-read latency and its distribution, repeatable burst, clocks synchronised
Report alongside those: how the clocks were synchronised and the residual error, whether ECT(1) and CE survived in each direction, the congestion control in effect, and the versions at both ends. That list is short enough to include every time and complete enough that somebody else can repeat the run.
Everything above applies to a wire. On a shared radio medium three additional things are true, and a measurement that ignores them is describing an empty room.
The medium is shared and half duplex, so a radio transmits or receives and never both, and every other device in range competes for the same air. A test on an idle channel measures the best case of a condition that does not occur in a building at eight in the evening. Competing load has to be real contention from real MACs, not simply more streams from the same station, because a second stream from the same radio queues behind the first rather than contending with it.
Airtime is the scarce resource rather than bits, since the air charges per transmission and a slow station occupies the medium for longer than a fast one carrying the same bytes, with the rate behind each modulation index tabulated in the MCS index tables. A throughput number without the airtime that produced it cannot be interpreted, and two systems with identical throughput can leave very different amounts of the medium available to everyone else.
And the delay a station experiences includes an interval nobody on the infrastructure side chose: the wait to win the medium. That is why responsiveness under working load is the honest wireless metric, and why an uplink measurement and a downlink measurement are different experiments rather than two halves of one.
A measurement of one path says nothing about a different path, a different hour, or a different set of neighbours, and repeating it does not make it general. Nothing here determines capacity, which is a property of a deployment rather than of a link. And a result that is reproducible can still be irrelevant, which is the failure mode of benchmark culture: an easily repeated number that no user would recognise. The defence is to measure the thing the user experiences, under load, against a real clock, and to publish enough that somebody can disagree with you precisely.
iperf2 is at sourceforge.io, its options are documented in the manual page, and the Android client that surfaces the L4S marking checks is at iperf2.fi-wi.com. It installs from Google Play.