Purpose
This note is the first scheduling note to memorize. Most system arguments about “too much load”, “not enough headroom”, or “wrong place to optimize” reduce to a handful of algebraic identities:
- utilization law
- Little’s Law
- bottleneck law
They are not queueing-model assumptions. They are bookkeeping identities for stable systems.
Model
Take one service stage with:
- arrival rate requests/second
- throughput completions/second
- service time seconds/request
- response time seconds/request
- average number in service
- average number in the whole system
For a stable stage, long-run throughput equals long-run arrival rate:
If the stage is unstable, the queue is growing and the averages are not stationary enough to be useful.
Utilization Law
Apply Little’s Law only to the server itself, not the queue. The average number of jobs “inside the server” is the utilization:
For a single server, is the busy fraction, so .
With service rate :
This is the cleanest way to check whether a plan is impossible. If a stage needs 3 ms of CPU per request and you want 500 requests/second from one core:
That is impossible on one core before queueing is even discussed.
Little’s Law
For any stable subsystem:
Average concurrency equals throughput times average time spent inside.
This identity is more useful than it first looks because the subsystem can be chosen freely:
- the whole service
- one queue
- one RPC stage
- one disk
- the decode queue of a model server
If a service does 20,000 requests/second and p50 end-to-end time is 50 ms, the average concurrent requests implied by the mean relation is on the order of
That does not say the distribution is narrow. It says any steady-state design claiming 20k QPS with only a handful of in-flight requests is wrong.
Bottleneck Law
Suppose request visits stage an average of times and each visit costs service time . The total demand per request at stage is
Then stage cannot sustain throughput above
for one server, or
for identical servers.
The system throughput limit is the minimum across stages:
This is the operational form of “the bottleneck wins.”
Worked Example
Suppose one request does:
- 2 ms of CPU
- 6 ms of network-bound downstream wait on a pool of 4 workers, each spending 6 ms of service
- 1 ms of local serialization
The demands are:
and
with , so
and
The CPU side is the bottleneck. Speeding up the RPC pool changes little until CPU demand drops.
flowchart LR IN[Arriving requests] --> CPU[CPU stage: demand 3 ms, max 333 req/s] CPU --> RPC[RPC pool: 4 workers at 6 ms each, max 667 req/s] RPC --> OUT[Complete] style IN fill:#e3f2fd style CPU fill:#f9d0d0,stroke:#c00 style RPC fill:#e8f5e9
Why Queueing Starts Late and Then Gets Ugly Fast
Operational laws tell you where the cliff is. Queueing models tell you how the cliff looks. In many systems the shape is:
- low utilization:
- moderate utilization: queueing appears but is tolerable
- high utilization: tail latency explodes
That last step is why a bottleneck running at 95% can dominate the whole service even if all other stages are comfortable.
Tiny Python Helper
from dataclasses import dataclass
@dataclass
class Stage:
name: str
visits: float
service_s: float
servers: int = 1
@property
def demand_s(self) -> float:
return self.visits * self.service_s
@property
def max_throughput(self) -> float:
return self.servers / self.demand_s
stages = [
Stage("cpu", visits=1, service_s=0.003, servers=1),
Stage("rpc_pool", visits=1, service_s=0.006, servers=4),
]
for stage in stages:
print(stage.name, stage.max_throughput)
print("system_limit", min(stage.max_throughput for stage in stages))What This Note Does Not Do
This note does not predict the full latency distribution. It only tells you:
- whether capacity math is even plausible
- how much concurrency a measured throughput implies
- which stage is limiting throughput
That is enough to rule out many bad explanations quickly.