4 min read
Measure producer→consumer handoff across threads: queue.Queue, queue.SimpleQueue, and a collections.deque + Lock/Condition hand-roll. This lab reports messages/s on Linux localhost.
It is not the single-threaded deque-vs-list queue bake-off (lab 54) — that was same-thread push/pop. Here the cost is cross-thread synchronization.
Related links:
Lab honesty (1 Oct 2026 IST): Python 3.13.5. Affiliates: 0. No Docker. One producer + one consumer; sentinel None.
Verdict up front (200 000 msgs): SimpleQueue ~14.13 Mmsgs/s; deque+Lock batch64 ~3.09; deque notify-each ~1.58; Queue unbounded ~0.97; Queue maxsize=1024 ~0.88. Use Queue when you need blocking/task_done/join; SimpleQueue or a careful deque+Lock when both ends are yours and you want speed.
| Arm | Pattern |
|---|---|
Queue() | classic unbounded |
Queue(maxsize=1024) | bounded backpressure |
SimpleQueue | simple FIFO, no task_done |
| deque+Lock notify-each | Condition wake per put |
| deque+Lock batch64 | notify every 64 puts |
Script: lab-evidence/100-queue-vs-deque-handoff/results/run_lab.py.
| Arm | Mmsgs/s |
|---|---|
| SimpleQueue | 14.13 |
| deque+Lock batch64 | 3.09 |
| deque+Lock notify-each | 1.58 |
| Queue unbounded | 0.97 |
| Queue maxsize=1024 | 0.88 |
SimpleQueue leads by a wide margin on this box. Classic Queue pays for a richer API (mutex + conditions + unfinished-task tracking). Bounded maxsize adds a little more contention under a tight producer.
| n | Queue | SimpleQueue | deque batch64 |
|---|---|---|---|
| 50 k | 0.98 | 15.93 | 3.22 |
| 200 k | 0.97 | 14.13 | 3.09 |
| 500 k | 0.96 | 14.5 | 3.04 |
Ordering holds across sizes — good sign the ranking is not a one-shot noise spike.
Queue: get/put timeouts, task_done/join, maxsize backpressure — pick when workers need join semantics.SimpleQueue: fast FIFO for “just hand me items”; no unfinished-task counter.Queue(maxsize=1024) is slightly slower here because the producer sometimes blocks when the buffer fills. That is the point of bounded queues in production: protect memory at the cost of a few messages/s. Unbounded Queue and SimpleQueue can grow without limit under a slow consumer — fine for labs, risky for long-running services.
Both ends agree on a poison-pill (None here). Without it, get() waits forever after the last item. For typed payloads, use a dedicated sentinel object, not a value that might appear in data.
Queue for worker pools that join() on completion.SimpleQueue for high-rate handoff without join bookkeeping.Evidence: summary.json, summary.txt.
One Linux box. Int payloads only. Two threads. No multi-consumer fan-out. GIL still serializes Python bytecode — this measures sync overhead, not parallel CPU.
At 200 k messages, SimpleQueue ~14.13 Mmsgs/s led; deque batch ~3.09; classic Queue ~0.97. Pick Queue for the API; pick SimpleQueue (or a careful deque) when handoff rate is the product metric.
Lab evidence
Lab 1 Oct 2026 IST. Python 3.13.5. 200k msgs: SimpleQueue 14.13 Mmsgs/s; deque batch64 3.09; Queue unbounded 0.97; Queue maxsize1024 0.88. Cross-thread (not lab 54). Affiliates: 0. Evidence: lab-evidence/100-queue-vs-deque-handoff/.
Hands-on contextlib.ExitStack vs nested with and manual close: real cycles/s for N resources, measured on Linux localhost in this hands-on lab for SREs.
Hands-on1 Oct 20263 min read
Summary1 Oct 20263 min read
Summary1 Oct 20263 min read