Queues, Producers and Backpressure

The pattern most real async programs are built from, and the one number that stops a fast producer from burying a slow consumer.

Overview

The shape of almost every async program

Once a program does more than a single fan-out, it tends to settle into the same structure: something produces work, something else consumes it, and the two run at different speeds. A crawler finds URLs faster than it can fetch them. A log reader ingests lines faster than it can index them. A handler accepts requests faster than a downstream service can answer.

An asyncio.Queue is the piece that connects the two. Producers put items in, consumers get them out, and because both operations are awaitable, the queue is also the place where one side can wait for the other without either blocking the loop.

Queues, Producers and Backpressure

The pattern most real async programs are built from, and the one number that stops a fast producer from burying a slow consumer.

Producer and consumer, running together

These editors run inside a browser event loop that is already going, so the examples finish with await main(). In a standalone .py script — run with python file.py — you write asyncio.run(main()) instead, which starts a loop, runs the coroutine, and closes it. The two are the same program with different entry points.

example_01.pyPython
Output
produced 0  (queue holds 1)
produced 1  (queue holds 2)
produced 2  (queue holds 2)
    consumed 0
produced 3  (queue holds 2)
    consumed 1
produced 4  (queue holds 2)
    consumed 2
    consumed 3
    consumed 4

Read the queue-size column. It climbs to 2 and then stops: the producer put items 0 and 1 immediately, and when it tried to put item 2 the queue was full, so await q.put(2) suspended the producer until the consumer took one out. From then on the producer only advances when the consumer makes room, which is why "produced 2" appears interleaved with "consumed" lines rather than all the production happening first.

That pacing is the whole point, and it came from one argument.

Backpressure is the maxsize

maxsize=2 made the queue bounded, and a bounded queue is what creates backpressure: when it is full, put waits, so a fast producer is automatically slowed to the rate the consumer can keep up with. The producer never runs more than two items ahead.

Remove the bound — asyncio.Queue() with no maxsize — and the behaviour changes completely: the producer dumps all five items in instantly, the queue holds all of them, and only then does the consumer start draining. With five items that is harmless. With a producer reading a million-line file into an unbounded queue faster than the consumer can process it, the queue grows without limit and the program runs out of memory — a failure that looks like a leak and is really a missing bound.

So the size of the queue is not a tuning detail; it is the contract between the two sides. Bounded means "the producer may run at most this far ahead", and that sentence is usually what you actually want.

Fanning out to several consumers

One slow consumer is often the bottleneck, and the queue makes it trivial to add more: start several consumer coroutines on the same queue and each takes the next available item. The queue hands each item to exactly one consumer, so work is shared without any coordination between them.

The clean way to know when everything is finished is task_done and join. Each consumer calls q.task_done() after handling an item; the producer, after putting everything, calls await q.join(), which returns only once every item has been marked done. That removes the awkward sentinel bookkeeping and gives a single, reliable "all work drained" signal — the standard way to shut a worker pool down cleanly.

Why a queue and not just gather

gather and TaskGroup run a *fixed, known* set of coroutines at once. A queue is for the other case: an *open-ended stream* of work whose size you do not know in advance, arriving over time, to be processed by a bounded number of workers. Crawlers, pipelines, and anything that consumes a feed have that shape, and the queue is what decouples "how fast work arrives" from "how fast we handle it" — with the bound as the safety valve between them.

Where it goes wrong

An unbounded queue with a fast producer. It grows until memory runs out. Set maxsize to whatever "too far ahead" means for your program.

Forgetting the shutdown signal. Consumers looping on await q.get() wait forever once production stops, unless a sentinel or join tells them to stop. A program that hangs at the end is usually this.

Doing the slow work between get and task_done in a way that blocks the loop. The queue paces correctly, but a synchronous call in the consumer still freezes everything — see the blocking call.

Reading qsize and acting on it. By the time you branch on q.qsize(), another coroutine may have changed it. Let put and get do the waiting; the count is for observing, not for control.

Recall check

0 of 3

Say the answer out loud before you reveal it — recalling it is what makes it stick, and rereading it is not.

  1. What does this module say about “The shape of almost every async program”?

  2. What does this module say about “Producer and consumer, running together”?

  3. What does this module say about “Backpressure is the maxsize”?

Cheat sheet

Queues, Producers and Backpressure

Once a program does more than a single fan-out, it tends to settle into the same structure: something produces work, something else consumes it, and the two run at different speeds. A crawler finds URLs faster than it can fetch them. A log reader ingests lines faster than it can index them. A handler accepts requests faster than a downstream service can answer.

ASYNC PYTHON · vizlearn.in/async_python/queues_and_backpressure.html

About the author

Ashish Jangra builds and maintains VizLearn. Every module here is written and the visualisation behind it hand-built, so the numbers in a readout come from the same code that draws the picture. Corrections are genuinely welcome and get priority over everything else — if a page states something wrong, or an animation misrepresents what the algorithm does, get in touch.