skip to content

Thread Confinement & limitedParallelism

limitedParallelism carves a capped view out of a dispatcher, and limiting it to one gives you single-thread confinement for shared mutable state. It is the modern replacement for spinning up your own single-thread context.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

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

open as a page

How can you protect shared mutable state in coroutines using single-threaded confinement, and how does limitedParallelism(1) compare to newSingleThreadContext()?

level: middleimportance: should knowfreq 55%

basics

~20 s

Run all updates to the shared state on a dispatcher that uses only one thread, so they happen one at a time and never race. limitedParallelism(1) does this by borrowing one slot of an existing pool; newSingleThreadContext creates a dedicated thread you must close.

open as a page

You need to limit concurrent calls to a downstream service to 10. Why prefer Dispatchers.IO.limitedParallelism(10) over creating Executors.newFixedThreadPool(10).asCoroutineDispatcher()?

level: middleimportance: should knowfreq 40%

basics

~20 s

The limitedParallelism view reuses the shared IO threads and needs no shutdown, so it's cheaper and safer. A custom fixed pool creates 10 dedicated threads you must shut down yourself, and they sit idle when unused.

open as a page

Explain the precise semantics of limitedParallelism: how parallelism is counted, what happens to excess coroutines, and the pitfalls of chaining or misusing it (e.g. on Dispatchers.Main or Unconfined).

level: seniorimportance: should knowfreq 30%

basics

~20 s

It limits how many coroutines run at once on that view; extras queue and run as slots free up. Each call returns an independent view with its own limit. It's meant for multi-threaded dispatchers like IO/Default, not for Main or Unconfined.

open as a page

Compare single-threaded confinement (limitedParallelism(1)) against Mutex and the actor pattern for protecting shared mutable state under high contention. When does each win, and what are the failure modes?

level: principalimportance: nice to knowfreq 18%

basics

~20 s

Confinement runs all updates on one thread so they can't collide; a Mutex lets work run on many threads but takes turns at the critical section; an actor uses one coroutine reading a channel of messages. Confinement and actors serialize ownership; a Mutex guards a section.

open as a page