skip to content

When wrapping resource acquisition in withTimeout, how can a timeout leak resources, and how do you prevent it?

level: seniorimportance: should knowfreq 35%

answer

  1. Timeout can cancel right after acquire, before capture => leak
  2. Acquire outside withTimeout; bound only the work
  3. try/finally runs on the cancellation path
  4. Suspending cleanup needs withContext(NonCancellable)
  5. use { } for Closeable resources

basics

~20 s

If a timeout cancels the code right after a resource is opened but before you store or release it, the resource can leak. Acquire and release inside try/finally, and make sure cleanup still runs during cancellation.

solid answer

~50 s

A timeout can cancel the block at any suspension point, including the instant *after* a resource is opened but before it's assigned to a variable you'd later close. The classic leak: `val res = withTimeout(t) { openResource() }` — if cancellation lands as `openResource()` returns, the value is dropped and never closed. The docs explicitly warn about this. Mitigations: (1) acquire the resource *outside* `withTimeout` and only do the time-bounded work inside; (2) use `try/finally` so close runs on the cancellation path; (3) if close itself suspends, wrap it in `withContext(NonCancellable) { ... }` so the already-cancelling coroutine can still complete cleanup. Combine with `use { }` for `Closeable` resources where possible. The principle: never let the only reference to a resource live solely inside a cancellable region whose result might be discarded by a timeout.

code

kotlin · 10 lines
kotlin
import kotlinx.coroutines.*

suspend fun query(): Result = run {
    val conn = openConnection()                  // acquire OUTSIDE the deadline
    try {
        withTimeout(2000) { conn.runQuery() }     // only the work is bounded
    } finally {
        withContext(NonCancellable) { conn.close() }  // suspending close still completes
    }
}

go deeper

for a junior

Likely unaware of the acquire/cancel race and would write the leaky pattern.

for a middle

Uses try/finally and knows finally runs on cancellation for non-suspending close.

for a senior

Owns resource lifetime outside the deadline and routes suspending cleanup through NonCancellable.

for a principal

Establishes resource-ownership conventions and reviews APIs so deadlines can never leak connections/handles.

## The leak scenario `withTimeout` cancels its block at the next suspension point after the deadline. Cancellation can be delivered *after* a side effect (opening a file, acquiring a connection) but *before* the value is returned and captured by the caller. The kotlinx.coroutines docs call this out directly with an example like: ```kotlin var acquired = 0 class Resource { init { acquired++ }; fun close() { acquired-- } } withTimeout(60) { val r = Resource() // acquired++ delay(70) // cancelled here -> r is never closed -> leak r.close() } ``` If the timeout fires during `delay`, `r.close()` never runs and `acquired` stays incremented. ## Fix 1 — acquire outside, bound only the work ```kotlin val r = Resource() try { withTimeout(60) { useResource(r) } // only the usage is time-bounded } finally { r.close() // always runs } ``` The resource's lifetime is owned by a non-cancellable `try/finally`, so a timeout can't strand it. ## Fix 2 — try/finally inside, close on cancel path ```kotlin withTimeout(60) { val r = Resource() try { delay(70) } finally { r.close() // runs even when cancelled (close is non-suspending) } } ``` `finally` blocks run during cancellation, so a *non-suspending* close is safe here. ## Fix 3 — NonCancellable for suspending cleanup If cleanup itself suspends (e.g. an async flush/close), the coroutine is already cancelling, so the next suspension would immediately throw. Wrap suspending cleanup so it's allowed to finish: ```kotlin withTimeout(60) { val conn = openConnection() try { conn.query() } finally { withContext(NonCancellable) { conn.closeAsync() } // suspending close still runs } } ``` `NonCancellable` is a special `Job` that ignores cancellation, so the cleanup completes. ## Closeable helper For `java.io.Closeable`/`AutoCloseable`, `use { }` guarantees `close()` in a `finally`; combine with the acquire-outside pattern for the strongest guarantee. There's also `Closeable.use` / Kotlin's `use` extension. ## Principle Never let the *sole* reference to an acquired resource exist only inside a cancellable region whose result a timeout could discard. Own the lifetime in a non-cancellable scope, and route suspending cleanup through `NonCancellable`.

  • Why can't you rely on a finally that suspends to clean up during a timeout?
    The coroutine is already cancelling, so the suspending call in finally immediately throws CancellationException and cleanup is skipped. Wrap it in withContext(NonCancellable).
  • Is acquiring the resource inside withTimeout ever acceptable?
    Yes, if you guard it with try/finally and the close is non-suspending (or NonCancellable), so cancellation can't strand it before close runs.

saying these in an interview costs you the question

  • Assigning an acquired resource only inside withTimeout with no try/finally
  • Doing a suspending close in finally without NonCancellable
  • Believing cancellation never happens between acquire and capture
  • Not knowing NonCancellable exists
  • Ignoring use { } for Closeable resources

context