serversim
LearnSimulatorTemplatesPricing
…
…
Open menu
Learn↗Simulator↗Templates↗Pricing↗
My DocumentsDocs
Contents/Chapter 1313 / 16

Trap ① — one CPU-bound task freezes the loop

In a server that takes turns from a single seat, which request is the most dangerous one? Everyone agreed to use the counter briefly and step aside — then one person sits down to fill in a thirty-minute form.

See this first in FoundationsWhen does an event loop win, and when does it lose?→

Let us break that promise. One request does heavy CPU work — a big image transform, heavy crypto, an enormous JSON parse. Left is a thread pool, right an event loop, same traffic. Red is the heavy task.

① Why the pool survives

Preemption from chapter 6. The timer interrupt reclaims the CPU from the heavy task and hands it to another thread, so one lane gets busy while everyone else keeps slipping through. The OS enforces fairness.

② Why the loop collapses

The loop has no such lever: once a callback starts it runs to completion, so no other event is pulled off the queue. While red owns the CPU the rest simply pile up — watch the completion counter on the right stand still.

③ What this means in practice

On event-loop servers, never do heavy computation on the loop thread. Hand it to a worker thread or a separate process, or slice it and yield in between. This scene is the honest answer to "when are coroutines/Node a bad fit".

④ How heavy is heavy enough — in numbers

Put numbers on it. If one callback on the loop takes 10 ms, that loop serves at most 100 per second; at 50 ms, twenty. And since later requests wait for the current callback to finish, tail latency is pinned to your heaviest callback. On an event loop it is not the average but the single worst callback that sets quality.

⑤ How you find it

CPU utilisation alone hides this: the core is busy at 100% while throughput refuses to rise. So runtimes expose a separate signal — event loop lag in Node, blocking-call detectors in Reactor, thread states via JFR on the JVM. Find where the time for one turn of the loop spikes and you have your culprit.

⑥ There are only three fixes

First, move the heavy work off the loop — a worker thread, a separate process, or a queue. Second, chop it up and yield: split the loop body and hand control back between chunks so other requests can slip in. Third, change the model: if per-request computation is genuinely heavy, a thread pool fits better, because OS preemption gives you fairness for free.

SpeedInject CPU-bound

Try it in the simulator

  1. 1.See how one heavy request shakes a whole server — open 'three-tier basics' and find the client node's RPS slider (default 1,000, Spring CPU 63%).
  2. 2.Raise RPS to 1,500 and Spring's CPU gauge climbs to 91%; at 1,700 it hits CPU 99% (KPI panel p99 291ms) — that is roughly where one app instance's real ceiling sits.
  3. 3.Push to 2,000 and the accept queue overflows — queue-full drops begin and the KPI panel's Success falls to 89.2%. Watch Drop rate and Success together.
Try it in the simulator →

Terms in this chapter

run-to-completion
A started callback runs to its end and cannot be preempted.
cooperative scheduling
Progress depends on everyone yielding voluntarily — the event loop model.
blocking the event loop
One heavy task delaying every other request.
event loop lag
How late one turn of the loop ran; the most direct signal for this trap.
offload
Moving heavy work off the loop to a worker or separate process.

Cooperative scheduling collapses before uncooperative code.

The remaining problem

One more trap — not CPU this time, but the wrong kind of waiting →

← Previous

Chapter 12. Coroutines / async-await — callbacks in sync clothing

Next →

Chapter 14. Trap ② — a blocking call on the loop