iperf2 Android Client — User Manual

Last updated: August 15, 2026

Actively developed. The underlying iperf2 engine has two decades of real-world use behind it; this Android client is newer and still gaining features release to release, so the UI does shift between builds. Issue reports get chased down fast - usually the same day - and every fix runs through an automated regression suite before it ships. If something looks off, tell us rather than assume it's expected: iperf2-android-testing@umbernetworks.com.

A network-testing client for Android built on iperf2, with live meters designed to let you correlate throughput, latency, and congestion-signal metrics as they happen, rather than reading one number in isolation.

If you've only ever used a speed-test app before, the biggest difference here is that this app cares as much about responsiveness under load as it does about raw Mbps. A link can report a great download number and still feel terrible for a video call or a game, because something else on the connection is queueing up traffic and adding delay. This app's Bounceback test is built specifically to catch that — see below — and several of its live meters (the L4S ones in particular) exist to show why a path is slow, not just that it's slow.

Getting started

Download the APK from the SourceForge file listing (look for the iperf2*_android.apk file, not the big green "Download" button at the top of the page, which grabs a different default file). Open it from your device's Downloads or notification shade, and allow the one-time "install unknown apps" prompt.

Invited as a Google Play tester instead? Open the opt-in link you were sent on the same phone, signed into the same Google account you gave us - that's what Play Console checks, and the step that actually enrolls you (being on the invite list alone doesn't). Tap Become a tester, then install from the Play Store like any other app - it can take a little while (up to a couple hours) to show up there after you opt in. If Play says the app isn't available, it's almost always the Play Store app itself being signed into a different Google account than the one you opted in with - check that before anything else. It's the same app either way, just without the "install unknown apps" step SourceForge sideloading needs.

On first launch, set Host to the server you want to test against and pick a test kind. The app remembers your last 5 host/port entries via the dropdown, and your other settings persist across restarts.

The two test kinds

Every test is either Bounceback or Capacity, each independently run over TCP or UDP L4S via its own Transport selector.

Bounceback

Bounceback live meters showing RPS and working load up/down Live meters during a 30s Bounceback test with working load — tap for the full screenshot

Round-trip responsiveness testing (request/response, measured in RPS) — not idle-network latency. With the Working load toggle on, the app runs a concurrent background load underneath the responsiveness probes, so you can see how well round-trip times hold up when the link is actually busy — the same concept behind "responsiveness under working load" / bufferbloat testing.

MetricWhat it shows
RPSResponses per second, with avg/min/max
RPS (live, windowed)A windowed mean ± stdev band, so you can see whether responsiveness is stable or degrading under load
RTT min (kernel) / theoretical RPS ceilingShown under the RPS chart. Since each request waits for its reply before the next one is sent, 1000 / RTT-min is a hard ceiling on achievable RPS — comparing it against the observed RPS tells you whether a run is pacing-limited (well under the ceiling) or path-limited (pressed against it)
Request/Reply Time DistributionHistogram of individual request/reply times, with a CDF (cumulative distribution function — at each point on the x-axis, what fraction of requests finished by then) line overlaid — see Comparing runs below for what the save/overlay controls under it do

Over TCP, a congestion-control algorithm can optionally be selected (off by default, which just means no algorithm is requested and the kernel's own default is used). Over UDP L4S, Bounceback is a discrete stop-and-wait exchange with no congestion-control loop to switch, so there's no CCA choice for it.

Connection status & L4S health badges

A quick primer if L4S is new to you: normal internet queues handle congestion by either making packets wait (adding delay) or dropping them once a buffer fills up — both of which a video call or game feels as lag or a glitch. L4S (Low Latency, Low Loss, Scalable throughput) is a newer scheme where capable senders mark their own packets as "L4S-capable" (a header bit called ECT(1)), and any L4S-aware queue along the path can then respond to early congestion by marking those packets CE (Congestion Experienced) instead of delaying or dropping them. The sender sees the CE mark and backs off immediately, often before any human-perceptible delay builds up at all. The badges below exist because none of this is visible from throughput or RTT alone — they answer "is this actually happening on my path right now?".

Once a run connects, a colored dot and status line appear above the meters: green means connected with no retry needed, yellow means either the connection needed more than one attempt or a previously-good connection dropped mid-test, and red means it never connected at all. This row stays visible after the run stops or completes, the same as the meters below it, so you can review the final state without needing to catch it live.

Next to the status text, a small WiFi/Ethernet/Cellular icon shows which network interface the traffic actually used. It turns amber if that doesn't match a transport you explicitly forced — some devices report a live network connection without the requested interface bind actually taking effect at the socket level, so traffic can silently go out over a different interface than requested; the amber tint is how you'd notice.

Running Bounceback over its own UDP L4S transport specifically (not TCP, not plain UDP) can add up to three more badges after the network icon, each appearing only once there's a definitive result to show — nothing appears while a signal hasn't happened yet, rather than guessing:

Close-up of the connection status row showing the CE badge and two green confirmed-intact badges Close-up of the status row: WiFi icon, "CE" badge, and both directions confirmed intact
BadgeWhat it means
Green circle with CE letteringGreen circle, "CE"The path's queue has applied at least one congestion-experienced (CE) mark this run — confirms a congestion-capable queue is actually there. There's no matching red state for this one: an absent mark doesn't confirm anything (could mean an unmarked path, or just no congestion yet), so nothing is shown until a mark actually occurs
Green circle with an arrow and the number 1Green circle, arrow + "1"One per direction (an up arrow for your request reaching the server, a down arrow for the server's reply reaching you) — means that direction's ECT(1) mark arrived at the other end intact, i.e. confirmed L4S-capable end to end
Red-orange bottle silhouetteRed-orange bottleThe direction-matched counterpart to the green arrow badge above, in the same up/down-oriented slot (upright for the request direction, upside-down for the reply direction) — means that direction's ECT(1) mark was stripped ("bleached") somewhere along the path by a non-L4S-aware middlebox, instead of arriving intact

In practice: on most home and mobile connections you'll only ever see the two green confirmed-intact badges (or nothing, if the path never gets busy enough to say anything either way) — the red-orange bleached badge is what you're looking for specifically when diagnosing a network or middlebox that's silently discarding L4S markings.

Capacity's own UDP L4S stream gets the same treatment, just with one badge slot instead of two — Capacity is a single continuous flow (not a discrete request/reply pair), so there's only one direction's marking-integrity to report. The same green circle (this time plain "1", no arrow — there's nothing to disambiguate against) and red-orange bottle (upright, no flip) appear in the connection status row for Capacity/UDP L4S runs exactly like the Bounceback ones above.

Working load

Turning on Working load adds a background stream alongside the Bounceback probe, with its own independent controls:

ControlWhat it does
Upload / Download / BothWhich direction(s) the background load runs
WL transportTCP or UDP L4S for the load stream — independent of the main Bounceback probe's own transport
WL congestion controlTCP algorithm choice (cubic/reno/bbr/prague, subject to device support) under TCP, or Prague/Reno under UDP L4S
Working load up / downLive throughput chart for each active direction

Once running, a Pause/Resume working load button lets you stop the background load's writes mid-test — without tearing its connection down — so you can watch the Bounceback probe's own RPS/latency respond to load coming on and off, then resume it, all without restarting the whole run. This works for every direction now (upload, download, or both). While paused, the working-load chart shows a genuine gap rather than a misleading flat zero, so a deliberate pause never looks like the connection stalled.

Comparing runs

Long-press a completed run to save it into one of four A/B/C/D slots - this works for any test kind, not just Bounceback. Tap a slot to view that saved run instead of the live one; long-press an already-saved slot again to star it as the reference. The comparison controls below, though, are Bounceback-specific - they work off the request/reply time distribution only a Bounceback run produces, so starring a Capacity run as the reference still lets you flip between saved slots, just without these particular overlays:

ControlWhat it does
Overlay CDF / Overlay PDFDraws the reference run's own CDF (cumulative — % of requests finished by time X, see above) or PDF (probability density — the histogram's raw shape, not cumulative, so a spike stays a spike instead of a step) curve on top of the one you're currently viewing
Show EMDShades the area between the current run's CDF and the reference's — that shaded region is the Earth Mover's Distance (EMD) number shown alongside it, so you can see at a glance whether the two runs diverge across the whole distribution or just part of it, not just read a scalar. Turning this on also forces the reference's CDF line on, since the fill needs both curves
Zoom TailRescales the chart to just the slow tail (past this run's own 95th-percentile latency) — the whole-distribution view otherwise squeezes that region into an unreadably thin sliver at the right edge. Comes with its own "Tail (>95%): EMD …" stat, splitting the whole-distribution EMD into body + tail. Works standalone, without a reference

Capacity

Capacity live meters showing Throughput, RTT smoothed, and Load Delay Live meters during a Capacity test — tap for the full screenshot

Continuous throughput testing, either transport:

TCP

MetricWhat it shows
ThroughputInstantaneous and average Mbps
RTT smoothedSmoothed round-trip time, paired with a chart of current RTT riding above the connection's tracked RTT floor
Load DelayCurrent RTT minus the RTT floor — the delay attributable to load on the path
CE DurationCongestion-experienced marking duration (where supported by the path)
Write Block TimeTime the sender spent blocked on a full send buffer
RetriesTCP retransmissions
CWND / inFlight / NotSentCombined chart of congestion window, bytes in flight, and bytes queued but not yet sent

Congestion control algorithm is optionally selectable (off by default) from what's actually available on your device: cubic, reno, bbr, or prague (prague is greyed out on most stock devices — it needs a patched kernel most don't have).

UDP L4S

Tests the L4S (Low Latency, Low Loss, Scalable throughput) congestion-signaling path.

Capacity UDP L4S live meters showing CE marks/sec and Marking Probability Live meters during a Capacity/UDP L4S test — tap for the full screenshot
MetricWhat it shows
Throughput, RTT smoothed, Load Delay, CE DurationSame meaning as the TCP versions above
Packets/secUDP packet rate
CE marks/secRate of congestion-experienced (CE) marks received
Marking ProbabilityFraction of packets arriving CE-marked, with an estimated max flow rate (r = 1/p − 1 Mbps, per published L4S guidance) shown alongside it
Loss ProbabilityFraction of packets lost
Window / Inflight / QueuedCombined chart, the UDP-L4S counterpart of TCP's CWND/inFlight/NotSent view

Congestion control here is always either Prague (the default) or Reno — unlike TCP there's no "off", since UDP L4S always runs one of the two.

Reading the live meters

Most metrics are shown as a small dial paired with a time-series sparkline beside it, each with a time axis ("-Ns" to "now") derived from your configured Interval. The dial gives the current reading plus avg/min/max; the sparkline lets you see the trend and line up a spike in one metric against what else was happening at the same moment.

Other options

OptionWhat it does
Duration / IntervalTest length and reporting interval, in seconds — see the note on the public responde.* servers' own 60-second cap below
Reverse (-R)Server sends instead of the client (TCP/UDP L4S)
Force IPv6 (-V)Forces resolution/connection over IPv6 instead of the default IPv4-first behavior — useful for confirming whether a v6 path actually works on your current network. All responde.* test servers support IPv6
tcp_notsent_lowatCaps how much unsent data the kernel buffers, for more responsive write-blocking behavior (TCP)
Custom request/reply sizeOverride the default Bounceback request/response payload sizes

Permit key

For non-local hosts, the app already sends a permit key automatically and silently — you never need to touch anything to test against the built-in responde.* servers. This is only for pointing the app at your own permit-key-gated server (a different key than the app's own), which the automatic one can't authenticate to.

Tap Advanced, just under the Host/Port row, to reveal a Permit key field. It's per-host: whatever you type is remembered against the host currently in the Host field, and switching hosts (by typing a different one or picking from the dropdown) automatically swaps in that host's own saved key — Advanced even expands or collapses itself depending on whether the host you land on has one saved. The × button in the field clears it and forgets it for that host in one tap, rather than requiring you to backspace through it.

About the default test servers: the responde.* hosts that ship as the app's default targets exist to verify the app itself works end to end — connects, authenticates, renders the live meters correctly. They are not sized for load testing, so don't read anything into the throughput/RPS numbers you see against them. Point the app at your own iperf2 server if you want numbers that mean something for your own path. The 11 servers, one per region: 60-second test cap: these servers time out a test past 60 seconds - keep Duration at or under 60s against any responde.* host. Your own server can be configured however you like.

Running your own server

Build from the 2-2-2 branch of the iperf2 project with --enable-professional - Bounceback, UDP L4S, and the metrics this app relies on aren't available on an older release or a plain community build:

git clone https://git.code.sf.net/p/iperf2/code iperf2
cd iperf2 && git checkout 2-2-2
./configure --enable-professional
make && sudo make install

One process covers everything the app needs via --dual-transport, which also listens for whichever of TCP/UDP L4S isn't the main port's own transport, on the adjacent port (TCP on port N also gets UDP L4S on N+1). Bounceback and UDP L4S are negotiated per connection from what the app itself requests, so don't pass --bounceback or --udp-l4s on the server - -V additionally enables IPv6 (without it, the server only listens on IPv4):

iperf -s -p 5001 --dual-transport -V --enhanced

Set the app's own Port to 5001, matching the server's own base port above - don't set it to 5002 yourself. Selecting a transport that needs the L4S port (Capacity's UDP L4S mode, or Bounceback with its transport set to UDP L4S or plain UDP) shifts the Port field to 5002 for you automatically; switching back shifts it back.

Working load traffic (its own WL transport selector, default TCP) reuses the same port as the main test whenever both sides use the same transport - two TCP connections to one port coexist fine, told apart by their own distinct source ports, same as any other pair of TCP connections to the same destination. A port shift only happens when the two transports actually differ, since this server topology serves TCP and UDP L4S as separate listeners one port apart:

Bounceback transportWL transportPort relationship
TCPTCPSame port
TCPUDP L4SWL shifts +1 (needs the UDP L4S listener)
UDP (plain)TCPWL shifts -1 (needs the TCP listener)
UDP (plain)UDP L4SSame port (both UDP)
UDP L4STCPWL shifts -1 (needs the TCP listener)
UDP L4SUDP L4SSame port (both UDP)

Either way, the app computes this automatically - the single --dual-transport server above already covers all six combinations, nothing extra to run.

Without a permit key, that server is fully open - anyone who knows the hostname can connect. To gate access, add --permit-key-file - permit keys are a --enable-professional feature only, the flag doesn't exist in a community build. It reads a plain NAME=VALUE file (default path /etc/iperf2/permit-keys.env) where any line whose name contains PERMIT_KEY contributes its value as one accepted key - so PERMIT_KEY_FOO= and PERMIT_KEY_BAR= can both be valid at once, useful for rotating a key without a hard cutover:

# /etc/iperf2/permit-keys.env
PERMIT_KEY_FOO=a1b2c3d4e5f6...
PERMIT_KEY_BAR=9f8e7d6c5b4a...

Reloads on SIGHUP (kill -HUP $(pgrep -x iperf)) without a restart. Whatever key you put in the file, paste the same value into the app's own Permit key field above.

Under systemd, a minimal unit for the gated version:

[Unit]
Description=iperf2 server (dual-transport)
After=network.target

[Service]
ExecStart=/usr/local/bin/iperf -s -p 5001 --dual-transport -V --enhanced --permit-key-file
Restart=on-failure
User=iperf
Group=iperf

[Install]
WantedBy=multi-user.target