Producer-consumer with asyncio.Queue

Concurrency in Python services Mid-level Python 3.13+ PostgreSQL 17+ Reviewed By Best Latest Apps

Order the steps

Order a clean producer-consumer run.

  • Create a bounded queue and start N worker tasks
  • Producer awaits put() for each item
  • Stop the workers (cancel or Queue.shutdown)
  • Workers process items and call task_done()
  • Await queue.join()

Choose an answer, then open Show answer below to check it.

Show answer

Answer: A B D E C

  1. Create a bounded queue and start N worker tasks Workers wait on get().
  2. Producer awaits put() for each item Waits when the queue is full.
  3. Workers process items and call task_done() Counts completed work.
  4. Await queue.join() Returns when all items are done.
  5. Stop the workers (cancel or Queue.shutdown) Clean exit.

30-second answer

Create a bounded asyncio.Queue, start N worker tasks that loop on await queue.get(), process, and call task_done(), and have the producer await queue.put() items. The bound gives backpressure: the producer waits when workers fall behind. await queue.join() waits until every item is processed, then you cancel the workers. Exceptions in a worker should be caught per item so one bad item doesn't kill the worker.

Practise saying it · 30 s. No recording, just a timer

Async producer-consumer.

Say your answer out loud, then check which of these points you covered:

  • Bounded queue
  • N workers
  • task_done and join
  • Per-item errors
  • Shutdown

Deep dive

Python 3.13 adds Queue.shutdown() for clean termination.

N is the concurrency limit for the downstream resource.

Use a TaskGroup for workers so failures surface.

For work that must survive restarts, use a durable queue instead.

Three workers, bounded queue Ran on Python 3.13.5 · mypy --strict
Python 3.13
import asyncio


async def worker(q: asyncio.Queue[int], done: list[int]) -> None:
    while True:
        n = await q.get()
        try:
            done.append(n * n)
        finally:
            q.task_done()


async def main() -> None:
    q: asyncio.Queue[int] = asyncio.Queue(maxsize=5)
    done: list[int] = []
    workers = [asyncio.create_task(worker(q, done)) for _ in range(3)]
    for n in range(10):
        await q.put(n)
    await q.join()
    for w in workers:
        w.cancel()
    print(sorted(done))


asyncio.run(main())

Common mistakes

  • Unbounded queues.
  • Forgetting task_done, so join() never returns.
  • Letting one exception end a worker silently.

Senior take & follow-ups

Bounded queue, N workers, task_done/join, per-item error handling.

Follow-up questions

Think of your answer first, then open the hint.

  1. What does join() wait for?

    Show hint

    task_done() for every item that was put.

  2. When use a durable queue instead?

    Show hint

    When losing in-memory items on crash isn't acceptable.

Suggest a fix if something here is wrong or out of date.