r/Python • u/pkhatri9 • 1d ago
Discussion When do you prefer asyncio.Semaphore over an asyncio.Queue for limiting concurrency?
I've been thinking about concurrency control in asyncio.
A common pattern for limiting concurrent work is:
sem = asyncio.Semaphore(10)
async with sem:
await do_work()
But in many cases, couldn't the same problem be modeled by putting work into an asyncio.Queue and running a fixed number of worker tasks?
I'm curious how experienced Python developers decide between the two approaches.
Are there real-world situations where a semaphore is clearly the better abstraction than a worker queue? Are there meaningful differences in cancellation behavior, backpressure, fairness, task lifetime, or code complexity?
I'd especially be interested in examples from production async Python code.
7
u/Wonderful-Habit-139 23h ago
If you can use anyio (or trio but the former still works in asyncio), you can use a mixture of anyio.CapacityLimiter + TaskGroup.start method in order to limit concurrency without dealing with unbounded tasks.
Meaning you start a task with the start() method, inside the task you enter the capacity limiter’s context manager, then you call the task_status’ started() method before the actual work. As easy as that.
6
u/donk8r 1d ago
The difference that actually bites is task lifetime. The semaphore version usually gets written as gather over a list comprehension, so you've materialised one Task per item before any work starts. That's fine at ten items and it isn't at a million, where you fall over on memory long before concurrency is the problem. Workers pulling from a queue give you k tasks no matter how big the input gets.
Cancellation follows from that. Cancel a gather and you're cancelling thousands of tasks that never ran and were just parked on the semaphore. With workers you cancel k of them, and whatever's left is still sitting in one place you can look at.
1
u/thisismyfavoritename 20h ago
the main difference is that the queue wouldn't require the caller to wait for the whole task to finish. It's mostly an architecture decision, both could be used to achieve the same thing, depending how you implement it.
If sempahore works for your use case then use it as it'll be simpler IMO
2
u/Khavel_dev 22h ago
I default to Semaphore when the tasks already exist as a batch (a list of URLs to fetch, a pile of records to process) and I just want to cap how many run at once. You wrap the work in async with sem: and you're done. No worker lifecycle, no poison pills, no shutdown coordination.
Queue makes more sense when the work streams in over time and you don't know the full set upfront. Producer-consumer patterns, pipelines where one stage feeds the next. The built-in backpressure from maxsize is genuinely useful there because Semaphore can't give you that without building it yourself.
The practical difference I care about most is cancellation. With Semaphore, each task is its own coroutine and you can cancel it individually. With Queue workers, you need to drain the queue or send sentinel values to stop them, and partial cancellation (stop some work, keep other work running) gets messy fast.
64
u/gmes78 1d ago
Semaphores are for syncronization, queues are for communication.