Throughput: Requests, Packets and Bytes per Second
Throughput is work per unit time in whichever unit you are limited by — requests, packets or bytes — and it is tied to latency by Little’s law and bounded by loss and RTT through TCP’s congestion control, so measuring one without the other tells you very little.
The problem
Three units, three limits
Bytes per second is what a link carries and what iperf and NIC counters report. Packets per second is what NICs, drivers, switches and the kernel network stack process, and every packet costs roughly the same amount of that work whether it holds 64 bytes or 1,500. Requests per second is what an application serves, and each request may be one packet or a thousand. A system can be saturated in any one of the three while the other two look idle, and the diagnosis depends entirely on which.
A 10 Gbit/s link carries ~812,000 packets/s of full 1,500-byte frames, or 14.88 million packets/s of minimum-size 64-byte frames. A Linux kernel doing ordinary socket I/O manages roughly 1–2 million packets/s per core before offloads, XDP or kernel bypass (DPDK) are needed; small-packet workloads — DNS, game state, VoIP, a chatty RPC — hit the pps wall at a tiny fraction of the link’s bandwidth. The symptom is a NIC at 10 % bytes-utilisation with a CPU core pinned in ksoftirqd, and the fix is bigger packets (batching), more cores handling interrupts (RSS), or bypassing the kernel.
- bytes/s: the link, and bulk transfers. Reported by
iperf3,ip -s link,sar -n DEV. - packets/s: the NIC, driver and kernel stack. 1,500-byte frames at 10 Gbit/s ≈ 812 kpps; 64-byte ≈ 14.88 Mpps. Watch
ksoftirqd,/proc/net/softnet_stat. - requests/s: the application. Depends on packets per request and work per request. Reported by the app, the balancer,
wrk/k6.
Throughput and latency: Little’s law
Throughput and latency are distinct but not independent. For any stable system, Little’s law says: average number of requests in flight = throughput × average latency, L = λW. It holds for a single server, a thread pool, a connection pool, a queue, a whole data centre, with no assumptions about distributions. It is the arithmetic behind every capacity question.
Read it in each direction. A service at 1,000 req/s with 200 ms latency has 200 requests in flight at all times; it needs at least 200 concurrent workers, connections or event-loop slots, or it queues. Fix the concurrency and the law gives the ceiling: 50 database connections at 20 ms per query serve at most 2,500 queries/s — and if latency rises to 40 ms under load, the same 50 connections serve 1,250, which is why saturated systems lose throughput exactly when they need it. And a load test that reports 2,000 req/s at 10 ms latency was measuring 20 in flight; production at 800 req/s and 500 ms latency has 400 in flight and a pool of 100 — the pool is the wall, and the CPU never sees the traffic that is waiting for it.
1in_flight = throughput × latency # 1000 req/s × 0.2 s = 200 concurrent2throughput = in_flight / latency # 50 conns / 0.02 s = 2500 req/s max3latency = in_flight / throughput # 400 queued / 800 rps = 0.5 s average wait4 5# the trap: latency rises under load, so capacity falls when it is needed most650 conns / 0.020 s = 2500 req/s 50 conns / 0.040 s = 1250 req/sWhat loss and RTT do to TCP throughput
Above a few Mbit/s the sender is TCP, and TCP’s throughput is set by its congestion window, which loss shrinks and which grows by only one segment per RTT in congestion avoidance (Congestion Control: Protecting the Network). The Mathis relation captures the steady state for classic loss-based congestion control: throughput ≈ (MSS / RTT) × (C / √p), with C ≈ 1.22 and p the packet loss rate. Two facts fall out. Throughput is inversely proportional to RTT — the same loss rate hurts a cross-continent connection 100× more than a same-rack one. And it falls with the square root of loss: 0.01 % loss costs a lot, 1 % is catastrophic.
Numbers: MSS 1,460 bytes, RTT 100 ms. At p = 0.001 (0.1 % loss) the ceiling is ~4.5 Mbit/s; at p = 0.01 (1 %), ~1.4 Mbit/s; at p = 0.0001 (0.01 %), ~14 Mbit/s. On a 10 Gbit/s link. This is why a "slightly lossy" Wi-Fi or a link with a marginal optic makes long-distance transfers crawl while local ones barely notice (Packet Loss: Duplicate ACKs, Fast Retransmit and the RTO), and why BBR — which models bandwidth and RTT instead of reacting to loss — exists. It is also why retransmission and loss counters (ss -ti, nstat -a | grep Retrans) belong next to any throughput measurement.
throughput ≈ (MSS / RTT) × 1.22 / √p
RTT 100 ms: p = 0.01 % → ~14 Mbit/s RTT 1 ms: p = 0.01 % → ~1.4 Gbit/s
p = 0.1 % → ~4.5 Mbit/s p = 0.1 % → ~450 Mbit/s
p = 1 % → ~1.4 Mbit/s p = 1 % → ~140 Mbit/s
same loss, 100× the RTT → 1/100 the throughputMeasuring both
Measure throughput and latency together, always, because either alone hides the other. iperf3 -c host gives raw TCP bytes/s between two hosts (add -P 8 to see whether one stream is window-bound — see Bandwidth vs Latency); iperf3 -u -b 0 -l 64 hammers small UDP packets to find the pps ceiling. ping and ss -ti (rtt:, retrans:, cwnd:) give latency and loss on a live connection. wrk, hey and k6 give requests/s and a latency distribution; report p50, p99 and the concurrency you drove, or the number is meaningless.
The shape to look for: as offered load rises, throughput rises linearly while latency stays flat, then throughput flattens and latency climbs — that knee is capacity, and Little’s law tells you which resource ran out (in-flight = throughput × latency at the knee, compared with your pool sizes). Measuring past the knee produces a throughput number that looks good and a latency number that is unusable, which is how load tests lie.
- Bytes:
iperf3,ip -s link, NIC counters. Packets:sar -n DEV 1(rxpck/s),/proc/net/softnet_stat,ksoftirqdCPU. Requests: app metrics, balancer metrics,wrk/k6with latency percentiles. - On a live TCP connection:
ss -tishowsrtt,cwnd,retrans,bytes_acked— throughput, latency and loss in one line. - Find the knee: raise concurrency until latency starts climbing; that throughput is capacity; in-flight at the knee is the resource that saturated.
Key points
- Throughput has three units — bytes/s (link), packets/s (NIC and kernel), requests/s (application) — and any one can saturate while the others idle.
- Small packets hit the pps ceiling long before the bandwidth ceiling: 64-byte frames are 14.88 Mpps at 10 Gbit/s; a kernel core manages ~1–2 Mpps.
- Little’s law: in-flight = throughput × latency. It sizes pools, exposes hidden queues and explains why capacity falls as latency rises.
- TCP throughput ≈ (MSS/RTT) × 1.22/√loss: inversely proportional to RTT, falling with the square root of loss.
- 0.1 % loss at 100 ms RTT caps a connection at ~4.5 Mbit/s regardless of the link.
- Measure throughput and latency together; capacity is the knee where latency starts to climb.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why can a NIC be at 10 % and the box still be saturated?
Because the work is per packet, not per byte. Small packets at high rate exhaust the driver and kernel path — softirq CPU — long before the link’s bytes are used.
▸Why does latency rise before throughput falls?
When a resource saturates, requests queue; queueing adds latency first. Throughput only falls once queues overflow or timeouts convert waiting requests into retries.
▸Why does loss hurt long connections more?
After a loss, TCP halves its window and regrows it one segment per RTT. Recovery takes a fixed number of RTTs; at 100× the RTT it takes 100× the time, and the average window over that recovery is what throughput is.
How it fails
What the failure looks like from inside real software.
- DNS or game server pinned at 100 % on one core with the NIC at 8 % utilisation: pps limit on the softirq path; enable RSS/multi-queue or batch.
- Service capacity in production is 40 % of the load-test figure: the test ran at low latency and low concurrency; production latency × throughput exceeds the connection pool, and requests queue.
- Cross-region replication "only gets 5 Mbit/s on a 1 Gbit/s link": ~0.1 % loss on the path; Mathis ceiling; fix the loss or use multiple streams / BBR.
- Throughput fine, p99 latency terrible: the system is being measured past the knee; every extra request is queued, not served.
- Latency-sensitive traffic collapses when a bulk transfer starts on the same link: the bulk flow fills queues; bufferbloat, see Where the Time Goes: The Request Timeline.