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.

Medium Python 3.9+ Aug 9, 2026 Concurrency & performance 13 views 0 copies

Python code

21 lines
Python 3.9+
import 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

stdout
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

  1. Use `asyncio.wait` with a fixed pool size and `asyncio.create_task` for more granular control
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Concurrency & performance

Related tutorials and quizzes for this topic.