skip to content

You have a fast event producer and a collector that can fall behind (e.g. rendering live telemetry). How do you reason about buffer capacity and BufferOverflow choice, and what are the failure modes of each?

level: seniorimportance: should knowfreq 28%

answer

  1. First ask: lose nothing, keep freshest, or keep earliest?
  2. SUSPEND -> throttles producer (or grows source latency)
  3. DROP_OLDEST/conflate -> freshest, silent loss of middle values
  4. UNLIMITED -> OOM risk, no back-pressure
  5. Capacity is a latency/memory dial; small for DROP_*

basics

~20 s

Decide whether you must keep every event or only the latest. If every event matters, suspend or use a big bounded buffer. If only freshness matters, drop oldest with a tiny buffer. Avoid unlimited buffers because they can run out of memory.

solid answer

~50 s

The core question is data-loss tolerance. If **no event may be lost**, use `BufferOverflow.SUSPEND` with a finite capacity sized to absorb bursts; the failure mode is the producer being throttled (or, with an external source, growing source-side latency). If **only the newest matters** (telemetry, cursor, UI state), use `DROP_OLDEST` with a small capacity — even `conflate()` — so the collector always sees fresh data; the failure mode is silently dropping intermediate values. If **early events matter and later floods don't**, `DROP_LATEST` preserves the head of the backlog. Avoid `Channel.UNLIMITED` for unbounded producers: it never back-pressures and risks `OutOfMemoryError`. Capacity is a latency/memory dial: bigger buffers smooth bursts but let stale items queue (higher end-to-end latency). For UI, freshness usually beats completeness, so DROP_OLDEST/conflate is the common pick; for billing/audit streams, SUSPEND or a durable queue.

go deeper

for a junior

Can pick suspend vs drop at a basic level and knows unlimited buffers are risky.

for a middle

Maps each policy to keep-all / keep-freshest / keep-earliest and names the obvious failure mode.

for a senior

Sizes capacity to bursts, articulates the latency/memory trade-off, and rejects UNLIMITED for unbounded producers.

for a principal

Recognizes when buffer() is insufficient and escalates to sampling/batching/durable-queue architecture, and standardizes policy by stream class (UI vs audit).

## Frame it as a data-loss decision Before touching the API, answer: **must every value be delivered, or only the freshest / the earliest?** That single answer selects the overflow policy. ### Lossless: BufferOverflow.SUSPEND ```kotlin producer.buffer(capacity = 256) // default SUSPEND ``` - Guarantees delivery and order. - **Failure mode:** the producer is throttled to the collector's pace. If the producer is an external, time-sensitive source (network, sensor) you can't actually slow, suspension instead grows latency or backs pressure into that source — possibly causing timeouts or dropped connections you can't see. - Sizing: capacity should cover the **largest expected burst**, not steady-state. ### Keep freshest: BufferOverflow.DROP_OLDEST (or conflate) ```kotlin telemetry.buffer(capacity = 1, onBufferOverflow = BufferOverflow.DROP_OLDEST) // or simply telemetry.conflate() ``` - Collector always sees the **most recent** value; the producer never blocks. - **Failure mode:** intermediate values vanish silently. Bad for anything that must aggregate or count every event; fine for "render current state." ### Keep earliest: BufferOverflow.DROP_LATEST ```kotlin errors.buffer(capacity = 16, onBufferOverflow = BufferOverflow.DROP_LATEST) ``` - Preserves the first N items, ignores the flood after saturation. - **Failure mode:** newest data is invisible while saturated — wrong when recency matters. ### Anti-pattern: Channel.UNLIMITED for unbounded producers ```kotlin fastProducer.buffer(Channel.UNLIMITED) // never back-pressures ``` - No back-pressure, no drops — but the queue grows without bound. **Failure mode: OutOfMemoryError** or pathological GC pressure. Only safe when the producer's total output is provably bounded. ## Capacity as a latency dial - **Larger capacity** absorbs bursts and improves throughput, but items can sit in the queue → **higher end-to-end latency** and more memory. - **Smaller capacity** keeps latency low and forces decisions (suspend or drop) sooner. - With DROP_* policies a small capacity is usually *better* — a deep buffer of soon-to-be-stale values is wasted work. ## Decision shortcut | Requirement | Choice | |---|---| | Every event, can throttle producer | SUSPEND + burst-sized buffer | | Only latest state matters | DROP_OLDEST / conflate, small capacity | | First events matter, ignore floods | DROP_LATEST | | Must never lose & can't throttle | external durable queue, not just buffer() | The meta-point: `buffer()` configures in-memory back-pressure between two coroutines. It cannot create durability or magically reconcile a permanently-too-slow collector — that's an architecture problem (sampling, batching, or a persistent queue).

  • When is conflate() the right call over a larger DROP_OLDEST buffer?
    When only the single most recent value is meaningful (e.g. live UI state); a deeper buffer would just hold values about to become stale.
  • buffer() with SUSPEND still can't keep up permanently. What now?
    buffer() only smooths bursts. A permanently slow collector needs an architectural fix: sampling/throttling, batching, or an external durable queue.

saying these in an interview costs you the question

  • Defaulting to Channel.UNLIMITED to 'never lose data'
  • Using DROP_OLDEST for audit/billing streams that need every event
  • Assuming a bigger buffer always reduces latency
  • Thinking buffer() adds durability or can fix a permanently slow collector

context