Limit Concurrency with asyncio.Semaphore in Python
Use asyncio.Semaphore to cap how many async tasks run at once, throttling a batch of coroutines to a set concurrency limit.
Python code
21 linesimport asyncio
import random
async def fetch_data(i: int, semaphore: asyncio.Semaphore) -> str:
async with semaphore:
print(f"Task {i} starts")
await asyncio.sleep(random.uniform(0.1, 0.5))
print(f"Task {i} finishes")
return f"Result {i}"
async def main() -> None:
semaphore = asyncio.Semaphore(3)
tasks = [fetch_data(i, semaphore) for i in range(10)]
results = await asyncio.gather(*tasks)
print("All results:", results)
if __name__ == "__main__":
asyncio.run(main())
Output
Task 0 starts
Task 1 starts
Task 2 starts
Task 0 finishes
Task 3 starts
Task 1 finishes
Task 4 starts
Task 2 finishes
Task 5 starts
Task 3 finishes
Task 6 starts
Task 4 finishes
Task 7 starts
Task 5 finishes
Task 8 starts
Task 6 finishes
Task 9 starts
Task 7 finishes
Task 8 finishes
Task 9 finishes
All results: ['Result 0', 'Result 1', 'Result 2', 'Result 3', 'Result 4', 'Result 5', 'Result 6', 'Result 7', 'Result 8', 'Result 9']
How it works
The async with semaphore: block acquires the semaphore before the task body runs and releases it when the block exits. With the semaphore initialized to 3, only three coroutines can hold the token at once, so three tasks start immediately while the rest wait. Once a task finishes, it releases the semaphore, allowing the next waiting task to proceed. asyncio.gather runs the coroutines concurrently and collects their return values in order. The asyncio.run(main()) entry point starts the event loop and manages the semaphore's lifecycle.
Common mistakes
- Creating the semaphore inside each task instead of sharing a single instance
- Forgetting to use `async with` and manually releasing the lock on errors
- Setting the semaphore value higher than the number of tasks, making it ineffective
Variations
- Use `asyncio.wait` with a fixed pool size and `asyncio.create_task` for more granular control
- Use `asyncio.Semaphore` inside `async for` for processing a stream of inputs with a fixed worker count
Real-world use cases
- Throttling concurrent HTTP requests to an API so the server doesn't rate-limit you.
- Capping how many file downloads or database queries run at once in a data pipeline.
- Limiting parallel processing in a web scraper to respect the target site's load limits.
Sponsored
More from Concurrency & performance
- Benchmark list.append vs deque.append in Python medium
- Build a Python Performance Profiler That Generates Readable Reports medium
- Graceful Shutdown Executor Context Manager in Python medium
- How to Build a Producer-Consumer Pattern with asyncio.Queue in Python medium
- How to Cancel an asyncio Task with Graceful Cleanup in Python medium
- How to Convert Data in Parallel with ThreadPoolExecutor in Python easy
Keep learning
Related tutorials and quizzes for this topic.