← Back to Additional Topics

asyncio.Semaphore

Additional TopicsMidconcurrencypython

The Question

What is asyncio.Semaphore and when would you use it?

What a Strong Answer Covers

  • Limits concurrency in async." `async with` usage. Not thread-safe distinction.

Senior-Level Answer

An `asyncio.Semaphore` is a concurrency primitive that limits the number of coroutines that can execute a particular section of code simultaneously. It maintains an internal counter initialized to the limit. When a coroutine acquires the semaphore (`async with sem:`), the counter decrements. If the counter is zero, the coroutine suspends (yields to the event loop) until another coroutine releases it. Release increments the counter and wakes one waiting coroutine.

The critical distinction from a threading `Semaphore`: `asyncio.Semaphore` is non-blocking at the OS level. When a coroutine waits for the semaphore, it yields control back to the event loop, allowing other coroutines to run. It does not block the thread. A threading semaphore blocks the OS thread entirely, preventing any other work on that thread.

The primary use case is **rate-controlling concurrent I/O**. Without a semaphore, code like this fires all requests simultaneously:

```python await asyncio.gather(*[fetch(url) for url in 1000_urls]) ```

This opens 1,000 concurrent connections, exhausting file descriptors, overwhelming the target server, and triggering rate-limit errors. With a semaphore:

```python sem = asyncio.Semaphore(20) async def bounded_fetch(url): async with sem: return await fetch(url) await asyncio.gather(*[bounded_fetch(url) for url in 1000_urls]) ```

Now at most 20 fetches run concurrently, respecting server capacity and connection pool limits.

Other use cases: limiting concurrent database connections when not using a connection pool, throttling writes to a rate-limited external API, controlling concurrent file I/O on systems with limited file descriptors, and implementing worker-pool-style patterns within a single event loop.

`asyncio.BoundedSemaphore` is a stricter variant that raises `ValueError` if `release()` is called more times than `acquire()`—useful for catching bugs where a release happens without a preceding acquire. Always prefer `async with` over manual `acquire`/`release` to ensure release happens even on exceptions.

What Separates a 2/3 from a 3/3

2/3 — Passing but Incomplete

Correctly explains the counter mechanism and suspension behavior, gives a concrete use case (bounding concurrent HTTP requests), explains async with pattern.

3/3 — Strong Answer

Explains the non-blocking event-loop behavior (suspends coroutine, not thread), contrasts with threading.Semaphore, gives a realistic code example, mentions BoundedSemaphore, and explains why 'async with' is preferable to manual acquire/release.

Common Mistakes

  • Confusing asyncio.Semaphore with threading.Semaphore — the async version suspends coroutines, not threads
  • Not knowing when to use it — only knowing the definition without a use case
  • Forgetting to use async with, leading to missed releases on exceptions
  • Not distinguishing Semaphore from BoundedSemaphore

Follow-Up Questions

  • What is the difference between asyncio.Semaphore and asyncio.Lock? — Lock is a binary semaphore (count=1) — only one coroutine at a time. Semaphore allows N concurrent holders. Use Lock for mutual exclusion, Semaphore for rate/capacity limiting.
  • How would you use a semaphore to implement a connection pool? — Initialize Semaphore(pool_size). Each coroutine acquires before using a connection and releases after. If the pool is exhausted, coroutines queue on the semaphore automatically.
  • What happens if you call release() more times than acquire() on a regular Semaphore? — The counter exceeds its initial value, effectively increasing concurrency beyond the limit — a silent bug. BoundedSemaphore raises ValueError instead, catching the bug explicitly.
  • Can asyncio.Semaphore be used across threads? — No — asyncio primitives are not thread-safe. They must be used within the same event loop. For cross-thread coordination, use asyncio.Queue or run_coroutine_threadsafe.

Related Questions

  • CAP theorem
  • SQL vs NoSQL
  • Big O basics
  • N+1 problem
  • AI tools in workflow

Can You Explain This Cold?

Reading the answer is step one. Explaining it unprompted — under interview pressure — is what actually matters. Get AI-graded feedback on your answer with follow-up probes on your weak points.

Get Graded — Free Assessment
GrindQuestionsAITechnical interview assessment
TermsPrivacyAbout