skip to content

Dispatchers (Default/IO/Main/Unconfined)

Default is sized for CPU work, IO for blocking calls, Main for UI, and Unconfined is a special case you rarely want. Interviewers ask which one you would pick for a JDBC query or a JSON parse, and why the answer differs.

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

questions

5

What is a CoroutineDispatcher, and when would you use Dispatchers.Default versus Dispatchers.IO?

level: juniorimportance: must knowfreq 85%

answer

  1. Default = CPU-bound, pool ~= core count
  2. IO = blocking I/O, up to 64 threads
  3. Switch with withContext
  4. IO and Default share threads
  5. Wrong dispatcher -> thread starvation

basics

~10 s

A dispatcher decides which thread a coroutine runs on. Use Default for heavy CPU work like sorting or parsing, and IO for waiting on network or disk calls.

solid answer

~40 s

A CoroutineDispatcher is the CoroutineContext element that controls which thread (or thread pool) a coroutine and its resumptions run on. Dispatchers.Default is backed by a shared pool sized to the number of CPU cores, ideal for CPU-bound work (sorting, JSON parsing, hashing). Dispatchers.IO is backed by a larger, elastic pool (default up to 64 threads or cores, whichever is higher) tuned for blocking I/O such as JDBC calls, file reads, or blocking HTTP clients, where threads spend time parked rather than computing. You switch dispatchers with withContext(Dispatchers.IO) { ... }. Choosing IO for blocking calls prevents starving the small Default pool; choosing Default for CPU work avoids spawning excess threads. Both share threads under the hood, so switching between them is cheap and may not require an actual thread hand-off.

code

kotlin · 4 lines
kotlin
suspend fun report(id: Long): String {
    val rows = withContext(Dispatchers.IO) { db.fetchRows(id) }   // blocking I/O
    return withContext(Dispatchers.Default) { rows.summarize() }  // CPU work
}

go deeper

for a junior

Knows Default = CPU, IO = blocking calls, and that dispatcher picks the thread.

for a middle

Explains pool sizing rationale and uses withContext correctly to switch.

for a senior

Articulates thread starvation risk and that IO/Default share threads, making switches cheap.

for a principal

Reasons about pool capacity tuning, parallelism limits, and system-wide throughput trade-offs.

## What a dispatcher is A **CoroutineDispatcher** is an element of the **CoroutineContext** that determines *which thread or thread pool* a coroutine uses when it starts and every time it **resumes** after a suspension point. It is essentially a strategy for scheduling work onto threads. You combine it into a scope or `withContext` call: `withContext(Dispatchers.IO) { ... }`. ## The standard dispatchers - **Dispatchers.Default** — a shared thread pool whose size equals the number of CPU cores (minimum 2). Built for **CPU-bound** work that keeps a core busy: sorting large lists, parsing/serializing, image processing, cryptographic hashing. More threads than cores would only cause context-switch overhead, so the pool is deliberately small. - **Dispatchers.IO** — a pool designed for **blocking I/O**: reading files, JDBC/database calls, blocking HTTP clients, `Thread.sleep`-style waits. It allows up to 64 threads by default (or the core count if higher) because such threads spend most of their time **parked/waiting**, not computing. Using IO keeps blocking calls off the small Default pool. ## Why the distinction matters If you run a blocking JDBC call on `Dispatchers.Default`, you can occupy all CPU-sized threads with parked work and **starve** genuine CPU tasks — throughput collapses. Conversely, doing pure computation on IO wastes nothing but offers no benefit. Match the dispatcher to the work's nature. ## Shared threads `Dispatchers.IO` and `Dispatchers.Default` **share the same underlying threads**. Switching from Default to IO via `withContext` may reuse the current thread without a real hand-off, making the switch cheap. IO is conceptually a *view* over the shared pool with a higher parallelism cap. ```kotlin suspend fun loadUser(id: Long): User = withContext(Dispatchers.IO) { // blocking JDBC call belongs on IO jdbcTemplate.queryForObject("...", ...) } suspend fun crunch(data: List<Int>): Long = withContext(Dispatchers.Default) { // CPU-bound aggregation belongs on Default data.fold(0L) { acc, x -> acc + heavyTransform(x) } } ``` ## Key APIs - `Dispatchers.Default`, `Dispatchers.IO` — the dispatcher objects. - `withContext(dispatcher) { ... }` — suspendingly switches the dispatcher for a block. - `suspend` — marks functions that may suspend; dispatchers control where they resume.

  • Why not just run everything on Dispatchers.IO?
    CPU-bound work on IO can spin up many threads competing for the same cores, causing context-switch overhead. Default's core-sized pool maximizes CPU throughput.
  • Does withContext always switch threads?
    No. If the target dispatcher shares the current thread (e.g., Default->IO) and capacity allows, it may resume on the same thread; the switch is logical, not always physical.

Default is a small expert crew kept busy; IO is a big call-center floor where most agents are on hold waiting.

saying these in an interview costs you the question

  • Saying IO has unlimited threads
  • Claiming Default is for I/O
  • Thinking each dispatcher has a fully separate thread pool
  • Running blocking JDBC on Dispatchers.Default
  • Confusing dispatcher with launch/async builder

context

open as a page

What is Dispatchers.Main, what does it require, and how does Dispatchers.Main.immediate differ from it?

level: middleimportance: should knowfreq 60%

basics

~10 s

Dispatchers.Main runs coroutines on the app's UI thread so you can safely update the screen. Its immediate variant skips re-scheduling and runs right away if you're already on that thread.

open as a page

What does Dispatchers.Unconfined do, and why is it generally discouraged in production code?

level: middleimportance: should knowfreq 45%

basics

~10 s

Unconfined doesn't tie a coroutine to any particular thread. It starts on the calling thread, and after a pause it continues on whatever thread woke it up. That unpredictability makes it risky.

open as a page

How do you build a custom CoroutineDispatcher from a java.util.concurrent.Executor, and what must you manage about its lifecycle?

level: seniorimportance: should knowfreq 40%

basics

~10 s

Wrap an existing Java thread pool with asCoroutineDispatcher() to get a dispatcher coroutines can use. Because you created the pool, you are responsible for shutting it down when done.

open as a page

Compare passing a dispatcher to launch/async versus using withContext to switch dispatchers. When is each correct, and what are the structured-concurrency and performance implications?

level: principalimportance: should knowfreq 35%

basics

~20 s

Give launch or async a dispatcher to start a new background job on it. Use withContext to temporarily move an existing coroutine's work to another dispatcher and come back. withContext is best for switching inside a function.

open as a page