Skip to content
Back to blog

Latency vs Throughput Explained

CoreConceptJuly 17, 20267 min read

A dashboard can look “fast” while users still wait, and a load test can report huge requests-per-second while p99 checkout times explode. Latency is how long one request takes. Throughput is how many requests the system completes per unit time. They are related, but they answer different questions—and optimizing one can quietly destroy the other.

This guide is for engineers designing or debugging APIs under load. We follow one service—checkout-api handling POST /checkout—measure both metrics correctly, use Little's Law to couple them, watch a real saturation failure, and leave with a decision rule: set the latency budget first, then maximize sustainable throughput inside that budget.

Latency is per-request time; throughput is completion rate—coupled under load, not interchangeable
Latency is per-request time; throughput is completion rate—coupled under load, not interchangeable

Two numbers, two questions

When a user clicks Pay, they care about latency: the wall-clock time from request start to response. When the business asks whether Black Friday traffic fits, they care about throughput: how many successful checkouts per second the system can sustain.

A single worker that finishes each checkout in 100 ms can complete about 10 requests per second if it handles one at a time. Ten workers in parallel can raise throughput while keeping the same per-request latency—until a shared dependency (database, lock, CPU, connection pool) saturates. After that point, queues grow, latency rises, and throughput stops climbing.

Running example. Subject: checkout-api POST /checkout. Success: p99 latency ≤ 300 ms at a stated concurrency, with error rate near zero. Intentional failure: push concurrency past the knee so p99 blows up while average latency still looks acceptable.

Same checkout-api: user asks about wait time; ops asks about sustained completions
Same checkout-api: user asks about wait time; ops asks about sustained completions

Quick reference

  • Latency = time for one request (ms).
  • Throughput = completed work per time (req/s, messages/s).
  • Parallelism can raise throughput without changing per-request work—until shared resources saturate.
  • Averages hide tails; report percentiles for latency.
  • Always state the workload and concurrency when quoting either number.

Remember this

Latency answers “how long for one checkout?”; throughput answers “how many checkouts per second?”—they couple under load but are not interchangeable.

Latency: one request's journey

Latency is the time a single request spends in the system: queue wait + service time + downstream waits. For POST /checkout, that includes auth, inventory check, payment call, and response write. Network RTT, TLS, and client parsing sit outside the server's view unless you measure end-to-end.

Averages lie about user experience because latency distributions are usually right-skewed: most requests are fine, a few are very slow, and those slow ones pull the mean up without describing any typical request. Report p50 (median), p95, and p99. Service objectives almost always target a high percentile (“p99 under 300 ms”), not the mean.

Tail latency compounds across fan-out. If checkout calls five dependencies and each has a 1% chance of being in its own slow tail, the chance that the overall request hits at least one slow dependency grows quickly. That is why a healthy-looking p50 can still produce an unacceptable checkout p99.

One request's latency = queue wait + service time + downstream waits
One request's latency = queue wait + service time + downstream waits

Quick reference

  • Break latency into queue time vs service time when diagnosing.
  • p50 = typical; p99 = what frequent users eventually feel.
  • Fan-out makes individual tails show up in the aggregate request.
  • Measure client-visible latency for UX; measure hop latency for ownership.
  • Cold starts, GC, locks, and retries live in the tail—not the average.
Record end-to-end latency for one request
1# Server-side timing for one checkout (illustrative Node/Express middleware)2app.use((req, res, next) => {3  const start = process.hrtime.bigint();4  res.on("finish", () => {5    const ms = Number(process.hrtime.bigint() - start) / 1e6;6    console.log(JSON.stringify({7      route: req.path,8      status: res.statusCode,9      latency_ms: Math.round(ms),10    }));11  });12  next();13});
Prefer percentiles over the mean
1# After a load run, summarize latencies (pseudo)2# samples = [42, 45, 48, ..., 890]  # ms3#4# mean can look fine while p99 is terrible5# p50 ≈ median(samples)6# p99 ≈ percentile(samples, 0.99)7#8# SLO example: p99(latency) <= 300ms AND error_rate < 0.1%

Remember this

Latency is per-request time; report p50/p95/p99 because averages hide the slow path users still hit.

Throughput: how much work finishes

Throughput is the rate of completed work—successful POST /checkout responses per second, not attempted connections and not bytes on the wire unless that is the metric you chose. A system can accept traffic faster than it completes work; that gap becomes a queue and later becomes latency.

Raising throughput usually means more parallelism (replicas, workers, shards) or less work per request (cache hits, smaller payloads, batching). Batching is the classic trade: wait to fill a batch, raise items processed per second, and increase the wait each item sees. That may be correct for a settlement job and wrong for interactive checkout.

Always pair throughput with an error budget. “5,000 req/s” that includes 20% timeouts is not capacity—it is a failure mode producing garbage numbers. Sustainable throughput is the highest rate you can hold while meeting the latency SLO and error SLO.

Throughput rises with parallel workers until a shared bottleneck saturates
Throughput rises with parallel workers until a shared bottleneck saturates

Quick reference

  • Throughput units must match the work: req/s, messages/s, MB/s.
  • Count successes against the SLO, not raw accepted sockets.
  • Replicas buy throughput when the bottleneck is parallelizable CPU/IO.
  • Batching buys throughput by spending latency—name that trade explicitly.
  • Peak sustainable throughput ≠ peak attempted load.

Remember this

Throughput is successful completions per second under an SLO—not raw accepted traffic or a latency-free brag number.

Little's Law couples them

In a stable system (queues do not grow without bound), Little's Law relates concurrency, arrival rate, and latency:

L = λ × W

L is the average number of requests in the system (in flight). λ (lambda) is the arrival rate (throughput when stable). W is the average time each request spends in the system (latency).

For checkout-api: if you sustain 200 successful checkouts per second and each spends 0.05 s (50 ms) in the system on average, you need about 200 × 0.05 = 10 concurrent in-flight requests. If latency jumps to 200 ms while you try to keep 200 req/s, concurrency must rise to about 40. Connection pools, thread pools, and memory must absorb that jump—or the system queues harder and latency rises again.

Little's Law does not require a particular scheduler. It is a long-run identity for stable systems. Use it for capacity sanity checks: size pools from target_throughput × p99_latency with headroom, and treat rising concurrency at fixed offered load as a latency-regression alarm.

Little's Law: in-flight ≈ arrival rate × time in system (stable queues)
Little's Law: in-flight ≈ arrival rate × time in system (stable queues)

Quick reference

  • Stable system: arrivals ≈ completions; queues do not grow unboundedly.
  • L = in-flight count; λ = rate; W = time in system.
  • Higher latency at the same throughput demands more concurrency capacity.
  • Size pools from latency budgets, then validate with load tests.
  • Little's Law explains the death spiral: slowdown → more in-flight → more queueing → more slowdown.

Remember this

Little's Law (L = λ × W) means the same throughput at higher latency requires proportionally more in-flight capacity—or queues explode.

Failure: optimizing mean latency into a p99 cliff

Trigger. The team enables request batching on the inventory service “to improve performance,” packing up to 50 SKU checks per batch with a 40 ms collection window. Mean inventory latency drops. Symptom. Checkout p99 climbs from ~180 ms to over 900 ms during peak, while dashboards showing average latency still look green. Conversion drops; on-call pages on timeout rate, not on “slow average.”

Root mechanism. Batching raised inventory throughput and lowered average service time per SKU, but each checkout now waits for the batch window plus the slowest item in the batch. Queueing at the batcher adds delay exactly when arrival rate rises—the knee of the latency–throughput curve. Mean latency improved; the user-visible percentile did not.

Recovery. Disable or shrink the batch window for the interactive path; move batching to async reconciliation jobs. Re-run load at peak concurrency and gate release on p99 + error rate, not mean. Prevention. Separate interactive and batch workloads; set SLOs on percentiles; load-test to the knee and refuse to run production past it.

Batching raised mean speed and throughput—but checkout p99 hit the cliff
Batching raised mean speed and throughput—but checkout p99 hit the cliff

Quick reference

  • Trigger: batching interactive checks to chase mean latency/throughput.
  • Symptom: green averages, red p99 and timeouts at peak.
  • Mechanism: batch wait + queueing at the knee of the curve.
  • Recovery: split interactive vs batch paths; roll back the interactive change.
  • Prevention: SLO on p99; load-test to the knee; never promote on mean alone.

Remember this

Batching can raise throughput and lower mean latency while destroying checkout p99—gate releases on percentiles under peak load.

Which should you optimize first?

Set the latency budget from the user journey first. Checkout, search-as-you-type, and payment confirmation are latency-bound: a missed p99 is a failed experience even if the cluster is “busy and productive.” Then maximize sustainable throughput inside that budget—scale out, cache, shed load, or reduce work per request without crossing the percentile SLO.

Optimize for throughput first when the consumer is a batch pipeline, analytics ingest, or overnight settlement: wall-clock for one item matters less than items processed per hour, and controlled batching is a feature. Still measure latency so you know queue depth and failure SLOs; “infinite queue” is not free throughput.

If you must choose under incident pressure: protect latency for the interactive path (shed, degrade, serve stale where safe) before chasing raw req/s. A system that completes fewer checkouts correctly beats one that accepts everything and times out.

Decision: latency-first for interactive UX; throughput-first for batch—always with SLOs
Decision: latency-first for interactive UX; throughput-first for batch—always with SLOs

Quick reference

  • Interactive UX → latency SLO first, then throughput inside it.
  • Batch / ingest → throughput first, with explicit queue and lag SLOs.
  • Never quote a performance number without workload, concurrency, and percentile.
  • Scale out buys throughput; speeding the critical path can buy both.
  • Related: [What Happens After You Type a URL?](/blog/what-happens-after-you-type-a-url) for where network time sits in end-to-end latency.

Remember this

Choose latency-first for interactive checkouts and throughput-first for batch pipelines—always inside explicit percentile and error budgets.

Key takeaway

Latency is per-request time; throughput is completion rate. Little's Law ties them through concurrency, and saturation turns extra offered load into queues instead of useful work. Averages hide the failures users feel—optimize and release against percentiles at a stated concurrency.

Practice (25 minutes): pick an API you can load-test (or a local stub). Capture p50/p99 and successful req/s at low concurrency, then double concurrency until p99 rises sharply—the knee. Write L ≈ λ × W using your measured rate and mean latency and compare to observed in-flight/concurrent connections. Intentionally add a 50 ms artificial sleep or a small batch window, re-measure, and roll it back. You pass when you can point to the concurrency where throughput stopped helping and p99 became the binding constraint.

Share:

Related Articles

A shopping cart must remember items, so the product cannot be literally stateless. The useful design question is where t

Read

Checkout slows while product pages remain healthy. CPU is moderate, but the orders table shows rising lock waits and wri

Read

A product page feels instant on the second visit because some layer reused work from the first. The useful beginner ques

Read

Keep learning

Follow a structured path or browse all courses to go deeper.