Restaurant Kitchen Order Routing with NATS Queue Groups
A hospitality use case: routing incoming orders to the right kitchen station (grill, salad, dessert) using subject-based routing combined with queue groups so each station's cooks share the load evenly.
- nats
- hospitality
- queue-groups
- routing
- tutorial
The scenario
A restaurant's point-of-sale system needs to route each order line to the correct kitchen station — grill, salad, dessert — and within a busy station, spread tickets across however many cooks are working that station right now.
Step 1 — subject-per-station routing
docker run -d --name nats-server -p 4222:4222 -p 8222:8222 nats:latest
nats context add local --server="nats://localhost:4222" --selectnats sub "kitchen.grill.ticket" --queue grill-stationnats sub "kitchen.salad.ticket" --queue salad-stationnats pub kitchen.grill.ticket '{"orderId":501,"item":"ribeye, medium-rare"}'
nats pub kitchen.salad.ticket '{"orderId":501,"item":"caesar salad, no croutons"}'Each ticket goes only to the station it's addressed to — the grill subscriber never sees the salad ticket, because they're different subjects entirely, not just different queue groups on one subject.
Step 2 — a busy grill station: multiple cooks share the load
nats sub "kitchen.grill.ticket" --queue grill-stationfor i in $(seq 1 6); do nats pub kitchen.grill.ticket "{\"orderId\":$((500+i)),\"item\":\"burger #$i\"}"; doneThe 6 tickets alternate between the two grill cooks (terminals 2 and 5) — one order per cook at a time, never duplicated — while the salad station (terminal 3), still just one cook, receives none of this burst since it was published to a different subject.
Step 3 — an expo/dashboard view across every station at once
The head chef's expo screen needs to see everything, from every station, without joining any single station's workload:
nats sub "kitchen.*.ticket"This subscriber deliberately omits --queue — it's a plain wildcard subscription, so it receives a
copy of every ticket in addition to whichever station cook also receives it for cooking, rather
than competing for tickets.
Step 4 — a station temporarily has zero cooks
nats pub kitchen.dessert.ticket '{"orderId":505,"item":"tiramisu"}'With plain pub/sub (as used above), this ticket is silently dropped if no dessert cook is currently subscribed — acceptable for a live "cook now" ticket during service, but risky if the restaurant needs a guarantee that no ticket is ever lost even during a brief station gap. For that stronger guarantee, use the same JetStream-backed durable pull consumer pattern shown in the e-commerce fulfillment article, one stream per station.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Grill ticket shows up on the salad station's screen | Subject typo (e.g. kitchen.grll.ticket) at publish time | Centralize the station-subject mapping in the POS code so it can't drift from what cooks subscribe to |
| Two grill cooks both cook the same ticket | One of them subscribed without --queue grill-station, making it a plain wildcard/duplicate subscriber instead of a load-balanced one | Confirm every station cook's subscription uses the exact same queue group name |
| Expo screen misses some tickets | Expo subscriber accidentally used --queue, making it compete with cooks instead of seeing everything | Expo/monitoring subscribers must NOT use --queue — they need a full copy, not a share |
| A ticket sent to an empty station is lost | Expected with plain pub/sub — no cook was subscribed at that instant | If that's unacceptable, back the station's subject with a JetStream stream + durable consumer instead of plain nats sub |
Recap
| Concept | Takeaway |
|---|---|
kitchen.<station>.ticket | Subject-per-station gives correct routing with zero central routing logic |
--queue <station>-station | Load-balances tickets across however many cooks are on that station right now |
Plain wildcard sub (no --queue) | Right choice for a "see everything" dashboard that shouldn't compete for work |
| Empty station risk | Plain pub/sub drops tickets published while no cook is subscribed — use JetStream if that's unacceptable |