Why might withTimeout fail to cancel a long-running block on time, and how do you make CPU-bound work respect the deadline?
answer
- Cancellation is cooperative, not preemptive
- No suspension point => deadline ignored
- ensureActive() / isActive / yield() to cooperate
- Check every N iterations, not every one
- runInterruptible maps cancel to Thread.interrupt for blocking calls
basics
~20 sTimeouts only kick in when the code pauses at a suspend point. Pure computation that never pauses ignores the deadline. To fix it, periodically check if the coroutine is still active, or yield, so cancellation can take effect.
solid answer
~50 s`withTimeout` relies on **cooperative cancellation**: it requests cancellation when the deadline passes, but that request is only honored at a suspension point or an explicit check. A tight CPU-bound loop (e.g. heavy math, parsing) has no suspension points, so it runs to completion regardless of the timeout. To make such code respect the deadline, insert cooperation: call `ensureActive()` (throws `CancellationException` if cancelled), check the `isActive` property and bail out, or call `yield()` periodically (which both checks cancellation and gives other coroutines a turn). For genuinely blocking work, offload to a dispatcher (e.g. `Dispatchers.IO`) — but note that `withTimeout` still cannot interrupt a blocking JVM call; you'd need the underlying API to support interruption or to use `runInterruptible`. The deadline timer itself runs independently, so the timeout is detected on time; what's missing is the loop's cooperation to stop.
code
kotlin · 10 linesimport kotlinx.coroutines.*
suspend fun crunch(n: Int): Double = withTimeout(50) {
var x = 0.0
for (i in 0 until n) {
if (i % 4096 == 0) ensureActive() // cooperate periodically
x += kotlin.math.sqrt(i.toDouble())
}
x
}go deeper
May only know the happy path and be surprised the deadline can be ignored.
Knows suspension points are required and can add yield/ensureActive in a loop.
Explains cooperative cancellation precisely and handles blocking calls via runInterruptible or API-level timeouts.
Sets architectural guidance for enforceable deadlines across CPU-bound and blocking-IO boundaries, with monitoring.
## Cooperative cancellation, restated Kotlin coroutine cancellation is **cooperative**, not preemptive. `withTimeout` schedules cancellation of its child coroutine after the deadline. Cancellation flips the coroutine's `Job` to cancelling and arranges for `CancellationException` to be thrown — but only when the coroutine **suspends** or **checks** for cancellation. Without such a checkpoint, the computation runs to the end and the timeout appears to be ignored. ## Why a CPU loop ignores the timeout ```kotlin withTimeout(50) { var x = 0.0 repeat(1_000_000_000) { x += kotlin.math.sqrt(it.toDouble()) } x } ``` Nothing here suspends, so there's no point at which the pending cancellation can be delivered. The block finishes after the full loop; the 50 ms had no effect. ## Making it cooperative Three tools from `kotlinx.coroutines`: - **`ensureActive()`** — throws `CancellationException` immediately if the coroutine has been cancelled. Cheap; call it inside the loop. - **`isActive`** — a `Boolean` property of the coroutine scope; check it and exit/clean up yourself. - **`yield()`** — a `suspend` function that suspends momentarily; it checks cancellation (throwing if cancelled) and lets other coroutines on the dispatcher run. ```kotlin withTimeout(50) { var x = 0.0 for (i in 0 until 1_000_000_000) { ensureActive() // honors the deadline x += kotlin.math.sqrt(i.toDouble()) } x } ``` Call the check every N iterations (not every single one) to keep overhead negligible. ## Blocking JVM calls Suspension points cover *coroutine* suspension, not thread-blocking JVM calls. A blocking `socket.read()` or `Thread.sleep()` won't be cancelled by `withTimeout` either, even on `Dispatchers.IO`. Options: - Use an API that accepts its own timeout (e.g. socket read timeout). - Wrap the blocking call in **`runInterruptible { ... }`**, which maps coroutine cancellation onto thread interruption (`Thread.interrupt()`), so interruptible blocking calls throw `InterruptedException` on cancel. ## Key mental model The timer always fires on time; the gap is the *cooperation* of the workload. Design long computations and blocking integrations with explicit checkpoints (`ensureActive`/`yield`) or interruption (`runInterruptible`) so deadlines are enforceable.
- Will withTimeout cancel a blocking socket read on Dispatchers.IO?Not by itself — it's a thread-blocking call, not a coroutine suspension. Use a socket-level read timeout, or wrap it in runInterruptible so cancellation triggers Thread.interrupt.
- What's the difference between ensureActive() and yield()?ensureActive() only throws if already cancelled and doesn't suspend. yield() suspends briefly, checks cancellation, and gives other coroutines a turn on the dispatcher.
A timeout is like a referee blowing a whistle — but the player only stops at the next time they glance at the ref (a suspension point). Eyes glued to the ball means they run on.
saying these in an interview costs you the question
- Believing withTimeout preemptively stops any code
- Not knowing CPU loops need ensureActive/yield to be cancellable
- Thinking Dispatchers.IO makes blocking calls cancellable
- Calling ensureActive on every single iteration unaware of overhead being acceptable but missing the cooperative-check concept
- Never having heard of runInterruptible for blocking work