Mastering asyncio Patterns: Queues, Semaphores, Timeouts, and Cancellation
Learn advanced asyncio patterns in Python: queues for task coordination, semaphores for concurrency control, timeouts to prevent hangs, and cancellation for graceful shutdown.
20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.
- ✓Basic understanding of Python async/await syntax
- ✓Familiarity with asyncio event loop and tasks
- ✓Experience with coroutines and awaitables
- Use asyncio.Queue to coordinate producer-consumer workflows.
- Use asyncio.Semaphore to limit concurrent coroutines.
- Use asyncio.wait_for to enforce timeouts on async operations.
- Use task.cancel() and CancelledError for graceful cancellation.
Imagine a kitchen with multiple chefs. A queue is like a ticket system where orders are placed and chefs pick them up. A semaphore is a limit on how many chefs can work at the stove at once. A timeout is a timer that says 'if this dish takes too long, throw it out'. Cancellation is like a fire alarm that tells everyone to stop what they're doing and leave safely.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
In modern Python applications, asyncio is the go-to library for writing concurrent code using async/await syntax. While basic coroutines and tasks are straightforward, real-world systems demand more sophisticated patterns to manage work distribution, control concurrency, handle slow operations, and shut down gracefully. This tutorial dives deep into four essential asyncio patterns: Queues for producer-consumer workflows, Semaphores for limiting concurrent access, Timeouts for bounding operation duration, and Cancellation for cooperative task termination. You'll learn not only the APIs but also the pitfalls and best practices that separate production-ready code from toy examples. By the end, you'll be able to build robust, scalable async applications that handle backpressure, resource limits, and shutdowns with confidence.
1. asyncio.Queue: Coordinating Producers and Consumers
The asyncio.Queue is a FIFO queue designed for async tasks. It allows producers to put items and consumers to get items, with built-in support for backpressure via maxsize. When the queue is full, put() blocks until space is available. When empty, get() blocks until an item arrives. This pattern is ideal for web scrapers, data pipelines, or any workload where you want to decouple work generation from processing. Here's a basic producer-consumer example:
2. asyncio.Semaphore: Controlling Concurrency
When you have many tasks that need to access a limited resource (e.g., database connections, API rate limits), asyncio.Semaphore limits the number of concurrent coroutines. It works like a mutex but allows multiple acquisitions up to a count. Use it as an async context manager: async with semaphore: ... This ensures that only a fixed number of tasks enter the critical section at once. Example:
3. Timeouts with asyncio.wait_for
Network calls, file I/O, or user input can hang indefinitely. asyncio.wait_for wraps a coroutine and raises asyncio.TimeoutError if it doesn't complete within a given timeout. This is crucial for responsiveness. Example:
4. Cancellation: Graceful Shutdown and Task Cleanup
Cancellation in asyncio is cooperative. Calling task.cancel() schedules a CancelledError to be raised at the next await point inside the task. The task can catch this exception to perform cleanup. For graceful shutdown, you typically cancel all tasks and then gather them with return_exceptions=True to avoid propagating CancelledError. Example:
asyncio.current_task().cancelled() or by awaiting asyncio.sleep(0) to allow cancellation to be delivered.5. Combining Patterns: A Production-Ready Worker Pool
In real applications, you often combine these patterns. For example, a web scraper might use a queue to hold URLs, a semaphore to limit concurrent requests, timeouts on each fetch, and cancellation for shutdown. Here's a complete example:
6. Best Practices and Common Pitfalls
- Always set maxsize on queues to prevent memory blowup. 2. Use asyncio.Semaphore as a context manager to ensure release. 3. Prefer asyncio.wait_for over manual timeout logic. 4. Handle CancelledError properly: do cleanup, then re-raise. 5. Use asyncio.shield to protect critical sections from cancellation. 6. Avoid mixing asyncio with blocking calls; use loop.run_in_executor for blocking I/O. 7. Test cancellation and timeout behavior with unit tests. Common pitfalls include forgetting to call
queue.task_done(), using semaphore incorrectly with create_task, and not re-raising CancelledError.
The Unbounded Queue That Brought Down a Chat Service
- Always set maxsize on queues when producers can outpace consumers.
- Monitor queue sizes in production to detect backpressure.
- Use timeouts on queue operations to avoid indefinite blocking.
- Implement graceful shutdown to drain queues before exit.
queue = asyncio.Queue(maxsize=100)await asyncio.wait_for(queue.put(item), timeout=5)| File | Command / Code | Purpose |
|---|---|---|
| queue_basic.py | async def producer(queue, n): | 1. asyncio.Queue |
| semaphore_example.py | async def fetch_url(sem, url): | 2. asyncio.Semaphore |
| timeout_example.py | async def slow_operation(): | 3. Timeouts with asyncio.wait_for |
| cancellation_example.py | async def worker(name, duration): | 4. Cancellation |
| worker_pool.py | async def fetch(sem, url, timeout): | 5. Combining Patterns |
Key takeaways
Interview Questions on This Topic
Explain how asyncio.Queue works and when you would use it.
put() and get() that suspend until the operation can proceed. Use it to decouple work generation from processing, e.g., in a web scraper or data pipeline.Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.
That's Advanced Python. Mark it forged?
3 min read · try the examples if you haven't