Software Development

Little’s Law: Why Adding Threads to a Saturated System Makes Latency Worse

Picture the incident. Latency is climbing, the dashboards are orange, and someone on the call says the obvious thing: “We’re out of capacity. Let’s double the thread pool.” The change goes out. Ten minutes later, latency is higher than before.

If that has happened to you, it probably felt like bad luck or a cursed service. It was neither. It was arithmetic, and the arithmetic fits on a napkin. A single equation from queueing theory predicts the outcome before you touch the config, and it holds for almost any system you will ever run.

The One-Line Law

Little’s Law says that the average number of things inside a system equals the average rate at which they arrive, multiplied by the average time each one spends inside.

L = λ × W

Here L is the average number of requests in the system, λ is the arrival rate (and, in a stable system, the throughput), and W is the average time a request spends inside from entry to exit. If a service completes 200 requests per second and each takes 50 ms, then on average 200 × 0.05 = 10 requests are in flight at any moment.

John D. C. Little proved this in a 1961 paper in Operations Research, “A Proof for the Queuing Formula: L = λW”. The relationship had been used as a working assumption before that. His contribution was showing that it holds under remarkably general conditions: the three averages need to be finite and the underlying processes stationary, and that is essentially it.

Why the Universality Is the Whole Point

Most queueing results are fussy. They depend on Poisson arrivals, exponential service times, a particular scheduling order. Little’s Law does not care. Arrivals can be bursty, service times can be heavy-tailed, the queue can be first-in-first-out or random. As long as the system is stable, the equation holds.

It also does not care where you draw the boundary. The “system” can be the whole service, just the request queue, just the thread pool, or a single database connection pool. Apply it to any of them and it works. That flexibility is what makes it practical rather than merely elegant.

The Twist: What Happens When a Resource Saturates

Now rearrange the equation. Little’s Law says W = L / λ. Latency equals concurrency divided by throughput.

So far, so innocent. The trouble starts when something in your system is a hard bottleneck. A CPU core can only do so much work per second. A database can only run so many queries at once. Past that point, throughput stops rising no matter how much more concurrency you throw at it. λ hits a ceiling.

Once λ is pinned, look at the formula again. If L keeps growing and the denominator cannot, then W has to grow in step. Each extra thread does not buy extra work. It buys extra waiting. The new thread joins the line, and every request behind it waits a little longer.

That is the entire mechanism. No mystery, no pathology, just a fixed denominator.

We Measured It

Equations are persuasive, but I wanted to watch it happen. I wrote a small closed-loop harness on Temurin 25.0.1. Each simulated request burns about 2 ms of CPU (a calibrated loop, verified at roughly 1.97 ms) and then blocks for 8 ms to stand in for I/O. A fixed number of threads, N, each issue requests back to back. I swept N from 1 to 160 and recorded throughput and mean latency.

The sandbox had a single vCPU, so the ceiling is easy to predict. One core divided by 2 ms of CPU work per request caps throughput at 500 requests per second. The knee should appear where concurrency matches that ceiling times the unloaded latency: 500 × 10 ms = 5 threads. For classic sizing advice, this matches the familiar rule from Java Concurrency in Practice, where the thread count is cores × (1 + wait time / compute time), or 1 × (1 + 8/2) = 5.

Throughput vs number of threads

Median of 3 runs of 3 s per level (1 s warmup each), JDK 25.0.1 (Temurin), single vCPU, platform threads, ~2 ms CPU + 8 ms blocking per request. X axis is log scale. Self-run measurement, not a published benchmark.

Throughput climbs almost linearly until about 5 or 6 threads. After that it goes flat at roughly 480 requests per second, a little under the 500 ceiling because of scheduling and sleep overhead. Going from 12 threads to 160 threads, a thirteen-fold increase, gained essentially nothing.

Mean latency vs number of threads

Same runs as above. Both axes are log scale, so a straight line with slope 1 means latency grows in direct proportion to thread count. The dashed line is W = N / 500 per second, derived from the theoretical ceiling. Self-run measurement.

Latency tells the other half of the story. Up to the knee it barely moves, drifting from about 10 ms to 12 ms. Beyond it, latency scales with thread count almost exactly as predicted. At 40 threads the mean was 83.9 ms. At 160 threads it was 332 ms, within about 4 percent of the 320 ms the ceiling-based formula predicts.

Does the Law Itself Hold?

Yes, and it is satisfying to check. In a closed loop with no think time, the number of requests in the system equals N by construction. I measured throughput and latency independently and multiplied them.

Threads (L)Throughput λ (req/s)Mean latency W (ms)λ × W
197.310.271.00
5408.312.235.00
12483.624.8212.00
40477.283.8540.01
160483.1332.16160.47

The product tracks the thread count to within a fraction of a percent at every level, including far past saturation. The law does not weaken when the system is unhappy. It simply reports the damage.

Where Extra Threads Hurt Even More

My experiment is the gentle case. Each added thread only added waiting, and throughput held steady. Real systems often charge a second penalty on top.

Threads are not free. Each one carries stack memory, scheduler overhead, and, if they share state, lock contention. When contention grows with concurrency, throughput itself can fall past a peak, which is the retrograde behavior described by the Universal Scalability Law in our earlier piece on that topic. In that situation λ shrinks as L grows, so W = L / λ rises faster than linearly. I did not observe retrograde throughput in this CPU-bound test, so treat it as a known risk rather than something these charts prove.

There is also the question of what kind of system you run. My harness is closed: a fixed number of clients wait for replies before sending more. Many production services are open, meaning users keep arriving regardless of how slow you are. In an open system pushed past capacity, the queue does not settle at a bigger number. It keeps growing, which means the system is no longer stationary and the law’s stability requirement is violated. Latency then climbs for as long as the overload lasts.

Putting Little’s Law to Work

The practical payoff is that you can estimate hard-to-observe quantities from easy ones. Pick a boundary, pick any two of L, λ, and W, and you get the third.

BoundaryL meansλ meansW meansWhat you can derive
Thread poolBusy threadsTarget requests per secondTime a request holds a threadThreads needed = λ × W
Database connection poolConnections in useQueries per secondTime a connection is heldPool size that avoids waiting
In-flight HTTP requestsConcurrent requestsRequests per secondEnd-to-end latencyA sensible concurrency limit
Request queueQueue depthDequeue rateTime spent waitingWait estimate = depth / rate

Take the thread pool row. If you want to serve 400 requests per second and each request holds a thread for 25 ms, you need about 10 threads busy on average. Giving the pool 400 threads does not make the service faster. It only removes the safety valve that would have limited the queue.

Limits and Pitfalls

Little’s Law is exact, but it is easy to misuse. A few cautions are worth keeping in mind.

First, it describes averages. A healthy mean can hide a miserable tail, and the law says nothing about the 99th percentile. Second, W includes waiting, not just working. If you measure only the time inside your handler, you will undercount W and mis-size everything downstream. Third, the boundary must be consistent: L, λ, and W all have to describe the same system. Finally, the long-run requirement matters. During a sudden spike or a cold start, the averages are still moving and the equation is a poor guide.

Checklist: Before You Add Threads

  • Compute current concurrency as throughput × latency. If it is already below your pool size, the pool is not the limit.
  • Find the bottleneck resource (CPU, connections, a downstream service) and estimate its throughput ceiling.
  • Estimate the knee: ceiling × unloaded latency. Pools far above it mostly add queueing.
  • Check whether latency rises in proportion to concurrency. A straight line means you are saturated.
  • Cap concurrency at the knee and shed or queue the excess deliberately, rather than letting threads pile up.

What We Learned

Little’s Law, L = λW, states that average concurrency equals throughput times average time in the system, and it holds for almost any stable system regardless of arrival or service patterns. Rearranged as W = L / λ, it explains the thread-pool incident: once a bottleneck caps throughput, adding concurrency cannot raise λ, so latency has to rise in proportion. In a single-core test with a 2 ms CPU and 8 ms I/O request, throughput flattened near 480 requests per second at about 5 to 6 threads, matching the predicted knee of 5, while mean latency grew from roughly 12 ms to 332 ms by 160 threads. The measured product of throughput and latency matched the thread count at every level. So the practical habit is simple: size concurrency to the knee, not above it, and treat a latency curve that rises in a straight line as proof that you are saturated rather than short of threads.

For further reading, John Little’s own retrospective, “Little’s Law as Viewed on Its 50th Anniversary”, covers the proof’s history and its uses in operations and computer architecture.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button