Queueing theory (M/M/1 & M/M/c)
Queueing theory gives exact formulas for a handful of idealized systems. YourSimulation reproduces those systems as small node graphs, and its simulated numbers match the formulas — which is exactly why you can trust it on the messier systems that have no formula. This page maps the theory onto the engine and shows the validation.
Kendall notation
A queue is summarized as A/S/c:
- A — the arrival process (interarrival-time distribution).
- S — the service-time distribution.
- c — the number of parallel servers.
M means Markovian — exponential (memoryless) times. So M/M/1 is exponential arrivals, exponential service, one server; M/M/c is the same with c servers sharing one queue.
Mapping onto YourSimulation
An A/S/c queue is a four-node chain:
graph LR
src["source<br/>exp interarrival (A)"] --> q[queue] --> svc["resource<br/>c servers, exp service (S)"] --> out[sink]- arrival rate
- per-server service rate
serverson the resource is
Key formulas
Utilization — the fraction of server capacity in use:
The system is stable (queue does not grow without bound) iff
M/M/1 — mean wait in queue and mean queue length:
(
M/M/c — with
The Erlang-C formula itself is standard; the docs don't re-derive it. (The validation suite computes it directly to check the engine — see validation.test.ts.)
Beyond M/M/c: abandonment and routing
Real systems rarely make customers wait forever, and rarely have a single line. Two extensions matter most, and YourSimulation models both.
Abandonment (reneging) — the Erlang-A model. Add impatience to M/M/c and you get M/M/c+M, known as Erlang-A: each waiting customer also has an exponential patience and abandons if service doesn't start in time. Closed forms exist but are unwieldy; the practical effect is intuitive — abandonment acts as a relief valve, so the system stays stable even when reneging: { patience }. The behaviour is easy to sanity-check: under light load almost no one abandons (waits match M/M/c); as load climbs, the abandonment rate rises smoothly.
Routing — join-shortest-queue. With several parallel servers you can keep one shared line (M/M/c) or split arrivals across separate lines. Sending each arrival to the shortest line (JSQ) is near-optimal and, for a symmetric system, keeps the lines balanced so their utilizations converge. A branch in shortest-queue mode does exactly this — measuring whole-station occupancy (waiting + in service) with random tie-breaks. Probabilistic splitting, by contrast, lets lines drift out of balance and performs worse under the same load.
See the Blocks reference for how to wire reneging, routing, shared resource pools, and batching into a model.
Validation
These checks live in packages/engine/test/validation.test.ts and run on every commit, asserting the engine matches theory within tolerance (utilization to ±0.03, waits to ±10%). The "Engine" column below is the actual simulator output (mean ± ci95):
| System | Metric | Theory | Engine (mean ± CI) |
|---|---|---|---|
| M/M/1, λ=0.1, μ=0.125 (ρ=0.8) | utilization | 0.800 | 0.794 ± 0.011 |
| M/M/1, λ=0.1, μ=0.125 (ρ=0.8) | 32.0 | 30.80 ± 3.04 | |
| M/M/1, λ=0.1, μ=0.125 (ρ=0.8) | 3.2 | 3.07 ± 0.32 | |
| M/M/3, λ=1, mean svc 2.4 (ρ=0.8) | ≈2.589 (Erlang-C) | 2.60 ± 0.10 |
The M/M/1 figures come from docs/examples/mm1.json; the M/M/3 figure from an equivalent 3-server model. Every theory value sits inside the engine's confidence band. (Note: the mm1.json example uses a shorter horizon/fewer replications than the validation suite, so its CI is wider — the test suite uses a longer run to assert the ±10% bound reliably.)
Try it
npx tsx packages/engine/src/cli.ts run docs/examples/mm1.json --pretty
# utilization ≈ 0.8 on "svc"; avgWait ≈ 32 on "q"Where to go next
- Distributions — replace exponential service with realistic shapes (the "M" stops applying, the simulation keeps working).
- Discrete-event simulation — how the engine actually computes these numbers.
- Glossary — definitions of utilization, throughput, and more.