flowchart LR
A["producer"] --> B["await q.put(item)"]
B --> C{"queue full?"}
C -->|yes| D["producer SUSPENDS -- back-pressure"]
C -->|no| E["item queued"]
E --> F["await q.get()"]
F --> G{"queue empty?"}
G -->|yes| H["consumer SUSPENDS, costing nothing"]
G -->|no| I["consumer processes it"]
I --> J["q.task_done()"]
J --> K["await q.join() unblocks when every item is done"]
L["sentinel or cancellation"] --> M["is how consumers are told to stop"]
Why asyncio.Queue
asyncio.Queue enables:
- safe communication between coroutines
- backpressure (limit queue size)
Example pipeline
import asyncio
async def producer(q: asyncio.Queue):
for i in range(10):
await q.put(i)
print("produced", i)
await q.put(None) # sentinel
async def consumer(q: asyncio.Queue):
while True:
item = await q.get()
try:
if item is None:
break
print("consumed", item)
await asyncio.sleep(0.1)
finally:
q.task_done()
async def main():
q = asyncio.Queue(maxsize=5)
p = asyncio.create_task(producer(q))
c = asyncio.create_task(consumer(q))
await p
await q.join()
await c
asyncio.run(main())Tips
- Use a sentinel to stop consumers.
- Use
maxsizefor backpressure.
How the pieces fit together
Think of the queue as a conveyor belt between two workers. The producer places items on it with await q.put(item), and the consumer takes them off with await q.get(). Neither side needs to know how fast the other is working. If the belt is empty, the consumer is suspended and costs nothing. If the belt is full (because you passed maxsize), the producer is suspended until there is room. That second behaviour is called backpressure, and it stops a fast producer from filling your memory with unprocessed work.
Three calls work as a team:
putandgetmove items in and out.task_done()tells the queue that one item you took out has been fully processed.join()waits until every item that was ever put has hadtask_done()called.
In the example above, main awaits the producer, then q.join(), then the consumer. The finally: q.task_done() matters: even when the sentinel arrives and the loop breaks, the sentinel itself was taken off the queue and must be marked done, otherwise join() would wait forever.
Scaling to several consumers
You can add consumers without changing the producer. Start three consumer tasks and they share the work, each taking the next available item. With several consumers you need one sentinel per consumer, because each consumer stops when it sees one:
import asyncio
async def consumer(name: str, q: asyncio.Queue) -> None:
while True:
item = await q.get()
try:
if item is None:
return
await asyncio.sleep(0.1)
print(name, "handled", item)
finally:
q.task_done()
async def main() -> None:
q = asyncio.Queue(maxsize=5)
workers = [asyncio.create_task(consumer(f"w{i}", q)) for i in range(3)]
for i in range(9):
await q.put(i)
for _ in workers:
await q.put(None) # one stop signal per worker
await asyncio.gather(*workers)
asyncio.run(main())The order in which workers print may vary, but all nine items are handled exactly once.
Common mistakes
- Using
queue.Queuein async code. Itsget()blocks the whole thread and freezes the event loop. Useasyncio.Queue. - Forgetting
task_done(). Thenjoin()never returns and the program hangs. - Too few sentinels. With N consumers and one
None, the other consumers wait forever. - An unbounded queue. Without
maxsize, a fast producer can grow memory without limit.
When to use it
Reach for an asyncio.Queue whenever work is produced in one place and handled in another: a crawler that finds links and fetches pages, a service that receives requests and writes to a database, or any pipeline whose stages run at different speeds.
Check yourself
A consumer awaits `q.get()` on an empty `asyncio.Queue`. What is it doing?
Awaiting suspends the task without blocking the thread, which is why thousands of waiting tasks are cheap while thousands of waiting threads are not.
Show answer
B — Suspended — the loop runs other tasks meanwhile — Awaiting suspends the task without blocking the thread, which is why thousands of waiting tasks are cheap while thousands of waiting threads are not.
What does giving the queue a `maxsize` buy you?
Unbounded, a producer faster than its consumer grows the queue until memory runs out — and that failure looks like a slow leak rather than a queue problem.
Show answer
B — Back-pressure — a full queue suspends the producer — Unbounded, a producer faster than its consumer grows the queue until memory runs out — and that failure looks like a slow leak rather than a queue problem.
What does `await q.join()` wait for?
It tracks completion of the WORK, not merely that the queue has drained. An item removed but not yet processed still counts as outstanding.
Show answer
B — Every item that was put to have a matching `task_done()` — It tracks completion of the WORK, not merely that the queue has drained. An item removed but not yet processed still counts as outstanding.
Why use `asyncio.Queue` rather than `queue.Queue` inside an event loop?
Same reasoning as `threading.Semaphore` in async code: blocking the single thread stops the tasks that would have made progress possible.
Show answer
B — `queue.Queue` blocks the thread, which stalls every task including the consumer — Same reasoning as `threading.Semaphore` in async code: blocking the single thread stops the tasks that would have made progress possible.
🧪 Try It Yourself
Exercise 1 – asyncio.Queue Basics
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 2 – Async Producer-Consumer
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.Exercise 3 – Queue Size
Tab indents. Press Escape, then Tab, to leave the editor. Control plus Enter runs the code; Control plus Shift plus Enter submits it.
Press Run to see what your code prints.