A grocery checkout
Three tills and a stream of shoppers. The only question is whether they form one queue that feeds all three tills, or a queue at each till.
Same tills. Same shoppers. Same service times. The only thing that changes is how people wait.
Any unfamiliar word is in the glossary.
Why this example exists
In the model, a resource with "servers": 3 is one queue feeding three servers. That is the
single most misread keyword in the whole schema, and this page is here to show what the
misreading costs.
The second arrangement needs three separate resources, because that is what three separate
queues are. Modelling it with "servers": 3 would silently give you the first answer while you
believed you had the second.
What a shopper does
The difference is entirely in what happens when your line stops. In one queue, a shopper stuck behind someone with a price check is overtaken by nobody — the next free till simply takes the front of the line. In lanes, everybody behind that price check waits for it, while a till two metres away stands empty.
The two models
One shared queue:
# yaml-language-server: $schema=https://clockwright.dev/schema/model.json
name: "grocery, one queue for three tills"
horizon_seconds: 28800.0
warmup_seconds: 3600.0
resources:
- { id: checkout, servers: 3 }
products:
- id: shopper
pieces: 1
steps:
- resource: checkout
process_seconds: { kind: erlang, k: 2, mean: 100.0 }
releases:
- product: shopper
interarrival_seconds: { kind: exponential, mean: 40.0 }
A queue at each till. Three resources, three products, and the arrival stream split three ways — a shopper picks a till on arrival and stays in it:
# yaml-language-server: $schema=https://clockwright.dev/schema/model.json
name: "grocery, a queue at each till"
horizon_seconds: 28800.0
warmup_seconds: 3600.0
resources:
- { id: till_1, servers: 1 }
- { id: till_2, servers: 1 }
- { id: till_3, servers: 1 }
products:
- id: shopper_1
pieces: 1
steps:
- resource: till_1
process_seconds: { kind: erlang, k: 2, mean: 100.0 }
- id: shopper_2
pieces: 1
steps:
- resource: till_2
process_seconds: { kind: erlang, k: 2, mean: 100.0 }
- id: shopper_3
pieces: 1
steps:
- resource: till_3
process_seconds: { kind: erlang, k: 2, mean: 100.0 }
releases:
- product: shopper_1
interarrival_seconds: { kind: exponential, mean: 120.0 }
- product: shopper_2
interarrival_seconds: { kind: exponential, mean: 120.0 }
- product: shopper_3
interarrival_seconds: { kind: exponential, mean: 120.0 }
Three streams at a mean of 120 seconds each is the same total arrival rate as one stream at 40 seconds. The workload is identical; only the queueing is different.
What comes out
One run first, to show the shape of the document. Note that the shared model reports one resource and the lane model reports three — that alone tells you which arrangement you built.
{
"released": 720,
"completed": 718,
"cycle_seconds_mean": 296.1,
"resources": {
"checkout": {
"utilisation_pct": 85.5,
"mean_queue": 4.83,
"mean_wait_seconds": 193.4,
"served": 718
}
}
}
{
"released": 688,
"completed": 692,
"cycle_seconds_mean": 442.8,
"resources": {
"till_1": { "utilisation_pct": 87.9, "mean_queue": 3.29, "mean_wait_seconds": 414.4 },
"till_2": { "utilisation_pct": 86.3, "mean_queue": 3.20, "mean_wait_seconds": 381.9 },
"till_3": { "utilisation_pct": 80.2, "mean_queue": 1.54, "mean_wait_seconds": 200.8 }
}
}
Do not draw the conclusion from those two runs. They are one roll of the dice each, and they
did not even see the same number of shoppers — 720 against 688. A single run is an anecdote. To
compare two models you need cwright run, which repeats the model and reports a
range instead of a number.
cwright run grocery-shared.yaml --replications 20
cwright run grocery-lanes.yaml --replications 20
Twenty replications each, on the development laptop (AMD Ryzen 7 7840U):
| time in the shop | 95% interval | shoppers served | tills busy | |
|---|---|---|---|---|
| one queue, three tills | 3.38 min | [2.93, 3.84] | 727 | 84.0% |
| a queue at each till | 8.95 min | [7.87, 10.02] | 730 | 83.7 / 84.8 / 86.8% |
What it says
The same three tills, serving the same shoppers, take two and a half times longer.
The intervals do not come close to overlapping — 3.84 against 7.87 — so this is not the dice. And the workload really is identical: 727 shoppers served against 730, and the tills are equally busy either way, around 84%. The same work arrives, the same work gets done, the same machines do it.
What changes is how much of the waiting is wasted:
| people waiting, on average | |
|---|---|
| one queue | 2.53 |
| three lanes | 3.11 + 3.38 + 4.37 = 10.86 |
Over four times as many people standing around, for the same tills and the same shopping.
Why
In a lane system, at any moment some lanes are long and others are short, and the people in the long lanes cannot use the short ones. Look at the single run above: till_3 had 1.54 people waiting while till_1 had 3.29. That imbalance is pure loss — a till serving a short queue while somebody stands in a long one is capacity you paid for and are not getting.
One shared queue makes that impossible. A till cannot be idle while anyone is waiting, and nobody can be stuck behind a price check while a till stands free.
This is why banks, airport security and post offices use a single snaking queue, and why supermarkets increasingly do too. It is not a fairness measure, though it is fairer. It is straightforwardly faster, for free.
The modelling lesson
If you write "servers": 3 you have modelled the shared queue. If your real shop has a queue
at each till, that model will tell you the shop is two and a half times better than it is, and
nothing will warn you — the utilisation is right, the throughput is right, and only the waiting
is wrong.
Things worth trying
- Make one till slower than the others, and see how much worse lanes get. Shared queues absorb variation; lanes concentrate it.
- Add an express till by giving one resource a shorter service time and its own product.
- Push the arrival rate up towards capacity. The gap between the two arrangements widens sharply as the shop gets busier — congestion punishes imbalance more than it punishes load.
Related
- Resources —
servers, and what it does and does not mean - A petrol station — the same keyword used correctly, for a bank of pumps
- Dispatch rules — once there is one queue, what decides who goes next