Event-loop fairness and bounded turns
Reason about starvation, runnable queues, cooperative yields, and the latency cost of long handlers.
On this page
The question for today
What you’ll understand
- Distinguish runnable delay from external I/O delay.
- Design bounded handler turns without losing edge-triggered work.
- Estimate a conditional scheduling-delay budget.
An event loop can remain fully occupied and still provide terrible service. One connection may always have more bytes, one callback may perform a large computation, or one runnable task may continuously enqueue itself. The engineering question is not whether the loop is busy. It is whether every admitted class of work receives enough timely attention under stated load and scheduling assumptions.
A callback runs until it gives control back
On a single event-loop thread, a handler that computes for a long uninterrupted interval prevents other handlers on that thread from executing. Asynchronous syntax does not preempt ordinary CPU instructions. A wait that is already satisfied may also return immediately, so repeating an await-shaped operation does not necessarily yield useful scheduling opportunities. Inspect the runtime’s actual scheduling semantics and measure callback execution duration separately from time spent suspended. A quick happy-path handler can hide a pathological large-input branch.
Fairness policies define which ready item runs next and how much it may do. Round-robin ready queues, byte budgets, item budgets, and elapsed-time budgets each control a different quantity. A byte limit may fail to bound expensive parsing; a time limit needs clock checks and interruptible work units. Bound each turn and preserve continuation state. Offload truly blocking operations or long CPU work through an appropriate facility, while bounding that facility’s queue instead of simply moving overload out of sight.
Draining and fairness must cooperate
An edge-triggered readiness handler cannot process a small amount, forget the resource, and assume another notification will arrive for remaining data. Instead, retain ready state and put the unfinished resource back on an application-level ready queue. Later turns continue from the saved offset. Only a suitable nonblocking stopping observation allows it to return to waiting for readiness. This separates notification collection from fair execution and prevents the false choice between starving peers and losing progress.
Timers and cancellation also require dispatch time. A deadline becoming due does not imply its handler executes at that exact instant. Record event-loop lag by comparing an intended timer deadline with actual callback start using a monotonic clock. A large gap suggests scheduling or CPU contention rather than an external network delay. Instrument queue age by work class so that a high-throughput connection cannot conceal starvation of health checks, interactive traffic, or shutdown callbacks. Priority itself requires a policy against indefinite exclusion.
Bound work instead of promising universal fairness
Fairness guarantees are conditional. If admitted work exceeds sustained execution capacity, every request cannot receive a fixed latency merely because the dispatcher rotates its queue. Admission control, bounded queues, and overload responses must accompany scheduling. Separate fairness across connections from fairness across tenants: a tenant opening many connections can gain many turns unless the scheduler accounts at the tenant level. Measure waiting-time distributions and rejection rates together, since rejecting most work can make the remaining latency look excellent.
In the queue lab, press Reset, set arrival rate to 4 and service rate to 3, predict growth from the initial four jobs, then press Play. Changing scheduling order alone cannot reverse this positive fluid slope. For a separate draining run, press Reset again, explicitly set arrival rate to 2 and service rate to 3, predict the slope, then press Play. Each reset restarts the model rather than preserving the preceding backlog. This model does not represent round-robin selection, timer phases, priority, or individual starvation; it demonstrates the capacity condition that any fairness policy must face. On paper, compare an unlimited draining handler with one processing a fixed byte budget and explicitly recording that the connection remains ready for another turn.
Work it through
Budget a round of runnable handlers
A toy loop has exactly six continuously runnable handlers in a fixed round-robin order. Each turn takes at most 2 milliseconds of application work plus 0.1 milliseconds of dispatch overhead. No other callbacks, priorities, or blocking calls exist.
A handler that just missed its turn can wait behind five other handlers. Each consumes at most 2.1 milliseconds including overhead.
Multiply five by 2.1 milliseconds to get a conditional next-turn waiting bound of 10.5 milliseconds. The bound excludes the handler’s own next service time.
If one handler instead blocks for 40 milliseconds, the original per-turn assumption fails. Its extra delay can propagate to every peer; the 10.5-millisecond claim no longer applies.
A useful fairness budget states the bounded turn, the runnable population, and exclusions. The dispatcher name alone proves none of them.
The living notebook
Explore the model
Interactive lab · queue
Scheduling cannot create service capacity
Press Reset, set arrival rate to 4 and service rate to 3, predict backlog growth, then press Play. Compare a fresh run after another Reset with both rates explicitly set to 2 and 3. The lab starts each run with four jobs and models rate imbalance, not timer phases, round-robin turns, or starvation.
- Fluid backlog
- 4
- Net rate · λ − μ
- −1 /s
- M/M/1 mean residence
- 1 s
Fluid backlog starts at 4 and follows max(0, 4 + (λ − μ)t). The separate M/M/1 residence-time formula assumes Poisson arrivals, exponential service and λ < μ. Parameter changes pause playback and recompute the model at the current elapsed time. Reset restores both parameters and returns time to zero. Playback starts paused. Adjust either parameter, then choose Play or Step.
Look forFair turns and bounded admission solve different parts of responsiveness.
Pause & reason
Check your understanding
Your turn
Try it yourself
Four handlers rotate with a bound of 3 milliseconds of work and 0.2 milliseconds overhead per turn. Under the same fixed-population assumptions, bound next-turn waiting after a handler just misses its turn. Then explain what admitting forty more runnable handlers changes.
Take a moment to make a prediction or work it out on paper. Open a hint when you need one.
Give me a hint
Count other turns, and do not include the selected handler’s own service.
Compare with the solution
Three other turns give 3 × 3.2 = 9.6 milliseconds. With forty additional handlers, there are forty-three others and the analogous bound is 43 × 3.2 = 137.6 milliseconds. The policy still rotates fairly in this simplified sense, but its response bound worsens; admission and per-class scheduling need separate consideration.
Keep the idea
Take it with you
- Long uninterrupted handlers delay unrelated callbacks on the same loop.
- A fairness budget must preserve pending readiness and continuation state.
- Capacity limits, tenant policy, and scheduling-delay measurements complete the argument.
Read further
Original sources and deeper explanations to follow at your own pace.
Optional progress, saved in this browser when available. Revisit any lesson at any time.
Lesson not marked complete.