Skip to main content

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.

One queue for three tills against a queue at each till

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.

one queue for three tills — cwright simulate, replication 0
{
"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
}
}
}
a queue at each till — cwright simulate, replication 0
{
"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 }
}
}
warning

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 shop95% intervalshoppers servedtills busy
one queue, three tills3.38 min[2.93, 3.84]72784.0%
a queue at each till8.95 min[7.87, 10.02]73083.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 queue2.53
three lanes3.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.

tip

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.
  • 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