---
date: '2026-07-05'
description: Little's law, the hockey stick, and rate limiters
id: notes
modified: 2026-07-06 13:05:30 GMT-04:00
tags:
  - cs
title: queueing theory
created: '2026-07-05'
published: '2026-07-05'
pageLayout: default
slug: hinterland/prep/bt/08-queueing/notes
permalink: https://aarnphm.xyz/hinterland/prep/bt/08-queueing/notes.md
generator:
  quartz: v4.6.0
  hostedProvider: Cloudflare
  baseUrl: aarnphm.xyz
full: https://aarnphm.xyz/llms-full.txt
---
## route

This module turns overloaded systems into math you can say in an interview.

1. Read `picture`, `little's law`, and `m/m/1`.
2. Solve `mm1_metrics`.
3. Read `variance`.
4. Solve `pk_wq`, `kingman_wq`, and `simulate_fifo`.
5. Read `rate limiters`.
6. Solve `TokenBucket`, `SlidingWindowCounter`, and `BoundedQueue`.
7. Review [[hinterland/prep/08-queueing/notes.fc]].

Depth: `two_choices`.

## picture

Three symbols carry the module:

| symbol    | meaning                 |
| --------- | ----------------------- |
| $\lambda$ | arrival rate            |
| $\mu$     | service rate per server |
| $\rho$    | utilization             |

For one server:

$$
\rho = \lambda / \mu
$$

Stability requires $\rho < 1$. At $\rho \ge 1$, the queue has nonnegative drift and mean wait diverges.

```mermaid
flowchart LR
  Arrivals["arrivals lambda"] --> Queue["queue"]
  Queue --> Server["server mu"]
  Server --> Done["departures"]
  Queue --> Backpressure["backpressure / shed"]
  Backpressure --> Arrivals
```

Queues exist because arrival and service are variable. Deterministic arrivals plus deterministic service below capacity have zero wait. Real systems pay for utilization and variance.

## little’s law

$$
L = \lambda W
$$

Mean number in system equals arrival rate times mean time in system. No Poisson assumption. The only requirement is that `L`, `lambda`, and `W` describe the same boundary.

Uses:

- 500 req/s at 100 ms mean latency means 50 in flight.
- 2000 req/s at 50 ms means 100 in flight.
- if dashboards say 10,000 req/s and 200 ms but only 500 in flight, some queue is outside the measured boundary.

## m/m/1

For Poisson arrivals, exponential service, one server:

$$
W = \frac{1}{\mu - \lambda} = \frac{E[S]}{1 - \rho}
$$

Latency multiplier:

| rho  | W / E\[S\] |
| ---- | ---------- |
| 0.50 | 2          |
| 0.80 | 5          |
| 0.90 | 10         |
| 0.95 | 20         |
| 0.99 | 100        |

The curve is the hockey stick. Going from 50% to 99% utilization costs 50:1 in mean latency multiplier. At 99%, adding 1% capacity halves the multiplier from 100 to 50.

M/M/1 metrics:

$$
L = \frac{\rho}{1-\rho}, \quad L_q = \frac{\rho^2}{1-\rho}
$$

$$
W = \frac{1}{\mu-\lambda}, \quad W_q = \frac{\rho}{\mu-\lambda}
$$

Little’s identities:

- `L = lambda * W`
- `Lq = lambda * Wq`
- `L = Lq + rho`

## variance

Pollaczek-Khinchine for M/G/1:

$$
W_q = \frac{\lambda E[S^2]}{2(1-\rho)}
$$

The second moment matters. A small fraction of huge requests can dominate everyone else’s wait.

Kingman for G/G/1:

$$
W_q \approx \frac{\rho}{1-\rho} \cdot \frac{C_a^2 + C_s^2}{2} \cdot E[S]
$$

Read it as:

```text
wait ~= utilization blow-up * variability * service time
```

If load is moderate, reduce variability. If utilization is near one, buy capacity or shed load.

FIFO simulator, Lindley recursion:

$$
W_i = \max(0, W_{i-1} + S_{i-1} - (A_i - A_{i-1}))
$$

One pass. No event heap needed for a single FIFO server.

## rate limiters

### token bucket

State:

- tokens
- last timestamp
- rate `r`
- capacity `B`

Lazy refill:

<div class="notebook-runtime" data-notebook-runtime="notebook-runtime-1149kwp"></div>

<div class="notebook-code-cell" data-notebook-cell-frame="code-cell-1" id="code-cell-1" data-notebook-language="python">

<div class="notebook-runtime-cell" data-notebook-cell="code-cell-1" data-notebook-execution-count=""><span class="notebook-execution-prompt" data-notebook-execution-label="code-cell-1" aria-live="polite">In [ ]:</span></div>

<div class="notebook-cell-actions" data-notebook-cell-actions="code-cell-1">
<span class="notebook-language-badge notebook-language-badge-python" data-notebook-language="python" title="Python cell"><span class="notebook-language-icon" aria-hidden="true"><svg class="notebook-language-svg notebook-python-icon" viewBox="0 0 111 112" aria-hidden="true" focusable="false"><path fill="#3776ab" d="M54.918785.00091927421C50.335132.02221727 45.957846.41313697 42.106285 1.0946693 30.760069 3.0991731 28.700036 7.2947714 28.700035 15.032169v10.21875h26.8125v3.40625h-36.875c-7.792459 0-14.6157588 4.683717-16.7499998 13.59375-2.46181998 10.212966-2.57101508 16.586023 0 27.25 1.9059283 7.937852 6.4575432 13.593748 14.2499998 13.59375h9.21875v-12.25c0-8.849902 7.657144-16.656248 16.75-16.65625h26.78125c7.454951 0 13.406253-6.138164 13.40625-13.625v-25.53125c0-7.2663386-6.12998-12.7247771-13.40625-13.9374997C64.281548.32794397 59.502438-.02037903 54.918785.00091927421zM40.418785 8.2196694c2.769547 0 5.03125 2.2986456 5.03125 5.1249996-.000002 2.816336-2.261703 5.09375-5.03125 5.09375-2.779476-.000001-5.03125-2.277415-5.03125-5.09375-.000001-2.826353 2.251774-5.1249996 5.03125-5.1249996z"/><path fill="#ffd43b" d="M85.637535 28.657169v11.90625c0 9.230755-7.825895 16.999999-16.75 17h-26.78125c-7.335833 0-13.406249 6.278483-13.40625 13.625v25.531247c0 7.266344 6.318588 11.540324 13.40625 13.625004 8.487331 2.49561 16.626237 2.94663 26.78125 0 6.750155-1.95439 13.406253-5.88761 13.40625-13.625004V86.500919h-26.78125v-3.40625h40.187504c7.792461 0 10.696251-5.435408 13.406241-13.59375 2.79933-8.398886 2.68022-16.475776 0-27.25-1.92578-7.757441-5.60387-13.59375-13.406241-13.59375zm-15.0625 64.65625c2.779478.000003 5.03125 2.277417 5.03125 5.093747-.000002 2.826354-2.251775 5.125004-5.03125 5.125004-2.76955 0-5.03125-2.29865-5.03125-5.125004.000002-2.81633 2.261697-5.093747 5.03125-5.093747z"/></svg></span><span class="notebook-language-label">Python cell</span></span>
<button type="button" class="notebook-icon-button" data-notebook-run-cell="code-cell-1" aria-label="Run code-cell-1" title="Run code-cell-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M8 5v14l11-7z"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-edit-cell="code-cell-1" aria-label="Edit code-cell-1" title="Edit code-cell-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="m4 16.5-.5 4 4-.5L19 8.5 15.5 5z"/><path d="m14 6.5 3.5 3.5"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-save-cell="code-cell-1" aria-label="Save code-cell-1 locally" title="Save code-cell-1 locally" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M5 4h11l3 3v13H5z"/><path d="M8 4v6h8V4"/><path d="M8 20v-6h8v6"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-revert-cell="code-cell-1" aria-label="Revert code-cell-1 local edit" title="Revert code-cell-1 local edit" hidden><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 14 4 9l5-5"/><path d="M4 9h10.5a5.5 5.5 0 0 1 0 11H11"/></svg></button>
<button type="button" class="notebook-icon-button" data-notebook-vim-cell="code-cell-1" aria-label="Enable Vim mode" title="Enable Vim mode" hidden><svg class="notebook-vim-icon" viewBox="0 0 602 734" aria-hidden="true" focusable="false"><g transform="translate(2 3)"><path class="notebook-vim-icon-left" d="M0 155.5704 155-1l-.000003 728L0 572.237919z"/><path class="notebook-vim-icon-right" d="M443.060403 156.982405 600-1l-3.181208 728L442 572.219941z" transform="translate(521 363.5) scale(-1 1) translate(-521 -363.5)"/><path class="notebook-vim-icon-cross" d="M154.986294 0 558 615.189696 445.224605 728 42 114.172017z"/></g></svg></button>
<span class="notebook-local-source-status" data-notebook-local-source-status="code-cell-1" hidden></span>
</div>

<div class="notebook-source-editor" data-notebook-source-editor="code-cell-1" hidden></div>

```python shell
tokens = min(B, tokens + (now - last) * r)
last = now
if cost <= tokens:
  tokens -= cost
  allow
else:
  deny
```

<div class="notebook-runtime-output" data-notebook-output="code-cell-1" hidden></div>

</div>

Guarantee over interval `T`: admitted cost `<= B + r*T`.

If `cost > B`, it can never be admitted. Return false up front.

### sliding-window counter

Estimate true trailing-window count with two aligned counters:

```text
estimate = previous * (1 - elapsed / window) + current
```

Memory: two counters per key.

Tradeoff:

- exact for aligned windows.
- approximate for true trailing windows.
- can be about 2x wrong in the worst bunched-arrival case.

### bounded queue

Pick a policy:

- reject new work.
- drop oldest.
- block producer.
- shed by deadline or priority.

Unbounded queues convert overload into unbounded latency and eventual OOM. Bounded queues make overload visible while the caller can still choose.

## tails and fan-out

Parallel fan-out multiplies tail risk.

If each of `n` leaves is fast with probability `p`, all leaves are fast with probability:

$$
p^n
$$

At 100 leaves, per-leaf p99 gives `0.99^100 ~= 0.37`. Only 37% of root requests see no slow leaf; the leaf p99 has become near the root median.

Hedging sends a duplicate after a high percentile delay and keeps the first result. It needs a budget. Hedging at p50 doubles load for vibes, which is illegal under the engineers’ statute of not being silly.

## load balancing

Power of two choices:

- choose one random server: max load about $\log n / \log\log n$.
- choose two random servers and pick the lighter: max load about $\log\log n / \log 2$.

One extra probe buys a large tail reduction. More than two probes gives smaller marginal wins.

## benchmarking

Open-loop load: arrivals continue at a fixed rate even when the system slows. This matches public internet traffic.

Closed-loop load: each virtual user waits for a response before sending the next request. This hides saturation because arrival rate falls when latency rises.

Coordinated omission: the load generator stalls during the system stall, so the requests that would have observed the queue never enter the histogram. Fix by measuring against intended send time.

## guards

- Little’s law requires matching measurement boundaries.
- utilization averages hide peak queues.
- retry counts multiply load during incidents; retry budgets cap it.
- pooling helps, except when cache locality, tenant isolation, or head-of-line blocking dominates.
- token bucket lazy refill should handle backward time by clamping.
- load tests should say open-loop or closed-loop.

## drills

1. 500 req/s at 100 ms latency means how many in flight?
2. M/M/1 at `rho = 0.95`: latency multiplier?
3. What happens at `rho = 0.99` if capacity rises by 1%?
4. Which term makes service whales hurt every request?
5. Token bucket `r=100/s`, `B=500`: max burst after idle?
6. Sliding log vs sliding counter memory.
7. Fan-out 50 leaves at per-leaf p99: chance no leaf is slow?
8. Define coordinated omission.

<script type="application/json" data-notebook-runtime-data>{"id":"notebook-runtime-1149kwp","sourcePath":"hinterland/prep/bt/08-queueing/notes.md","language":"python","indexUrl":"https://cdn.jsdelivr.net/pyodide/v0.29.4/full/","cells":[{"id":"code-cell-1","source":"tokens = min(B, tokens + (now - last) * r)\nlast = now\nif cost \u003c= tokens:\n  tokens -= cost\n  allow\nelse:\n  deny","language":"python","executionIndex":null}],"toolbar":false,"debug":true,"vimMode":true}</script>

