Reference · Mechanics · Contents

How thread starvation actually works

A short book on one bug: why a Java service caps at ~30 req/s with the CPU idle, the connection pool "healthy," and — eventually — the health check failing and nodes crash-looping. Read it in order, or jump to the part that's fuzzy.

Every page is self-contained, built from the real numbers and analogies we worked through, and designed to be re-read later. The whole mechanism is a chain: a tiny pool → a hard ceiling → an unfair queue → a runaway spiral → a rolling, fleet-wide outage. Each part is one link in that chain.

The chain, in one breath

The whole story

One missing argument sends async work to a pool of 3 threads shared by the entire JVM (Part 1). Because each request needs 2 of them and each call takes 50 ms, arithmetic — not any setting — caps the service at ~30 requests/second (Part 2). Push past that and work piles up in an unfair queue, so most requests are fast but a few rot for 15 seconds while the average hides it (Part 3). If requests arrive faster than 30/s, the backlog never drains — it runs away (Part 4). Because load is uneven, this hits some nodes and not others, disguising a universal bug as a per-node incident (Part 5). And when a node's front-end threads are all stuck, even the health check can't get answered, so the orchestrator kills healthy-but-starved nodes and the failure walks across the fleet (Part 6).

The six parts

  1. Two thread pools
    The 3 ForkJoin workers and the 200 Tomcat threads — and why blocking one pool freezes the other. What "200/200 busy" means.
  2. The throughput ceiling
    Where ~30 req/s comes from. It's not a setting — it's arithmetic (and it's Little's Law). Three ingredients, three ways to derive it.
  3. The signature of queueing
    p50 vs p99 vs the mean. Why the queue is a pile, not a fair line — so most requests are fast and 1% rot for 15 seconds.
  4. Overload & runaway starvation
    Arrival rate vs drain rate. The sink that overflows. 28 req/s is fine, 35 is doomed — and why the collapse accelerates.
  5. Blast radius: why certain nodes
    The bug is on every node, but only some fall over. Uneven load, the cliff, and the rolling cascade that disguises it.
  6. The health-check cascade
    Why a "healthy" process fails its health check, why the orchestrator then makes it worse, and the one-line fix that saves observability.
The one sentence to keep

The starved resource is invisible: there's no "30" to grep for, no metric on it by default, and every resource you'd normally blame looks healthy — which is exactly why this bug hides until it's an outage.

Primary sources for the whole series JDK 21 CompletableFuture javadoc (the default-executor rule) · ForkJoinPool javadoc (sizing & no compensation) · Little's Law (the ceiling & the mean).
These pages are your teacher in written form. If a part doesn't land, ask me in the chat — I can re-run the demo that produced any number here and show it happening live.