skip to content

What does Dispatcher.limitedParallelism(n) do, and why would you use it?

level: juniorimportance: should knowfreq 45%

answer

  1. View over a dispatcher, caps to n
  2. Borrows parent threads, no new pool
  3. n=1 => serialized confinement
  4. Throttle API / DB pool

basics

~20 s

It creates a view of a dispatcher that lets at most n coroutines run at the same time. You use it to cap how much work hits a limited resource, like a database or an external API.

solid answer

~40 s

limitedParallelism(n) is a CoroutineDispatcher method (on Dispatchers.IO, Default, etc.) that returns a new dispatcher view sharing the parent's threads but allowing at most n coroutines to execute concurrently. It doesn't create new threads; it borrows from the parent pool and queues the rest. Typical uses: throttling concurrent calls to a rate-limited HTTP API or bounding parallel DB queries so you never exceed a connection-pool size. Because IO has a large soft limit (default 64), an unbounded fan-out of IO work could starve threads; limitedParallelism(n) caps that slice without you managing a separate pool. With n=1 it serializes execution, giving single-threaded confinement for shared mutable state without a Mutex.

code

kotlin · 6 lines
kotlin
val dbDispatcher = Dispatchers.IO.limitedParallelism(8)

suspend fun loadUsers(ids: List<Long>): List<User> = coroutineScope {
    ids.map { id -> async(dbDispatcher) { repo.findById(id) } }.awaitAll()
}
// at most 8 DB calls run concurrently, regardless of ids.size

go deeper

for a junior

Knows it caps concurrent coroutines to n and is used for throttling a limited resource.

for a middle

Explains it's a view borrowing parent threads (no new pool) and gives concrete use cases like DB pool sizing.

for a senior

Discusses the IO default-64 soft limit, queueing semantics, and n=1 for confinement vs a Mutex.

for a principal

Reasons about resource budgeting across the app, why a view beats spawning custom pools, and lifecycle/no-close characteristics.

## What it is `CoroutineDispatcher.limitedParallelism(n)` returns a **new dispatcher** that is a *view* over the original. The view guarantees that **no more than `n` coroutines run in parallel** on it at any instant. Excess coroutines wait in a FIFO queue until a slot frees up. Key terms: - **Dispatcher**: the part of a coroutine's context that decides *which thread(s)* a coroutine runs on (e.g. `Dispatchers.IO`, `Dispatchers.Default`). - **Parallelism**: how many coroutines physically execute at the same instant. Distinct from *concurrency* (how many are in-flight/suspended). ## How it works - It does **not** allocate its own threads. It **borrows** worker threads from the parent dispatcher's pool, but enforces the `n` cap on top. - So `Dispatchers.IO.limitedParallelism(4)` runs on IO's shared threads but lets at most 4 of its coroutines run concurrently. - The cap is per-view: two separate `limitedParallelism` views don't share their counters. ```kotlin val apiDispatcher = Dispatchers.IO.limitedParallelism(4) suspend fun fetchAll(ids: List<Int>) = coroutineScope { ids.map { id -> async(apiDispatcher) { callRateLimitedApi(id) } }.awaitAll() } ``` Even if `ids` has 1000 entries, at most 4 `callRateLimitedApi` calls run at once. ## Why use it - **Throttle a limited resource**: rate-limited API, a fixed DB connection pool, a single GPU. - **Bound fan-out**: `Dispatchers.IO`'s default cap is 64 threads (`kotlinx.coroutines.io.parallelism`); a huge fan-out can hog them. A limited view reserves a controlled slice. - **Confinement** (`n=1`): serializes access to shared mutable state — see the single-threaded-confinement questions. ## Common gotchas - It is **not** a thread pool you should leak — but unlike `newSingleThreadContext`, the view itself holds no dedicated thread and **does not need `close()`**. - Calling `limitedParallelism` on `Dispatchers.Unconfined` or arbitrary dispatchers without proper support can throw or behave oddly; it's designed for the built-in dispatchers.

  • Does limitedParallelism(n) create new threads?
    No. It returns a view that borrows threads from the parent dispatcher's pool and only enforces the concurrency cap on top.
  • Do you need to close() the result of limitedParallelism?
    No. Unlike newSingleThreadContext, the view owns no dedicated thread, so there's nothing to close.

Like a nightclub with one bouncer: the building (thread pool) is big, but only n people get inside at once; the rest wait in line.

saying these in an interview costs you the question

  • Saying it creates a brand-new thread pool of size n
  • Confusing it with limiting the total threads of Dispatchers.IO globally
  • Thinking you must close() the returned dispatcher
  • Claiming two separate views share one concurrency counter

context