skip to content

Inside a callbackFlow, when do you use trySend versus send, and how do you handle backpressure?

level: seniorimportance: should knowfreq 55%

answer

  1. trySend = non-suspending, returns ChannelResult
  2. send = suspending, needs a coroutine context
  3. default channel is rendezvous -> trySend can fail
  4. shape overflow with buffer(capacity, onBufferOverflow)
  5. DROP_OLDEST/DROP_LATEST/SUSPEND for policy

basics

~10 s

Use trySend in a normal (non-suspending) callback because it never blocks; it just succeeds, fails, or signals closed. Use send only from suspending code that can wait. Control overflow with a buffer.

solid answer

~40 s

trySend(value) is non-suspending: it attempts to offer into the channel and returns a ChannelResult that is success, failure (buffer full / no capacity), or closed. It's the correct choice inside ordinary listener callbacks whose signatures can't suspend. send(value) suspends until there is capacity, so it can only be called from a suspending context (e.g., a coroutine you launch inside the block). The default callbackFlow channel is rendezvous-like (the buffered RENDEZVOUS), so a fast producer can have trySend fail. You shape backpressure with the buffer() operator or by passing onBufferOverflow (BufferOverflow.SUSPEND, DROP_OLDEST, DROP_LATEST) — typically callbackFlow(...).buffer(capacity, onBufferOverflow). Dropping with trySend silently discards events, which may or may not be acceptable; choose deliberately and consider checking the ChannelResult.

go deeper

for a junior

Knows trySend is used in callbacks and doesn't suspend.

for a middle

Distinguishes trySend (non-suspending, ChannelResult) from send (suspending, needs coroutine).

for a senior

Reasons about the default rendezvous channel, buffer()/BufferOverflow policies, and inspecting ChannelResult.

for a principal

Designs an explicit backpressure/overflow contract for adapters and weighs event-loss vs latency trade-offs system-wide.

## trySend vs send Inside `callbackFlow` you are in a `ProducerScope<T>`, which exposes both: - **`trySend(value): ChannelResult<Unit>`** — *non-suspending*. Immediately tries to put the value in the channel. Returns: - **success** when buffered/received, - **failed** when there's no capacity (buffer full / rendezvous with no waiting receiver), - **closed** when the channel is already closed. Because callbacks usually have a plain (non-`suspend`) signature, `trySend` is the natural fit. - **`send(value)`** — *suspending*. Waits until there is capacity. You can only call it from a coroutine, e.g. one you `launch { }` inside the block. Use it when you genuinely want backpressure to slow the producer and the producing path can suspend. ```kotlin callbackFlow { val cb = Api.Callback { v -> val result = trySend(v) // never suspends if (result.isFailure && !result.isClosed) { // buffer full: log/metric/drop decision } } api.register(cb) awaitClose { api.unregister(cb) } } ``` ## Backpressure & buffering The channel under `callbackFlow` defaults to a **rendezvous-style** buffer, so a fast async source can overrun a slow collector and `trySend` will fail. Reshape it with the **`buffer`** operator: ```kotlin callbackFlow { /* ... */ } .buffer(capacity = 64, onBufferOverflow = BufferOverflow.DROP_OLDEST) ``` - **`BufferOverflow.SUSPEND`** (default with a fixed capacity) — back-pressures the sender; with `trySend` this means failure once full, since trySend won't suspend. - **`BufferOverflow.DROP_OLDEST` / `DROP_LATEST`** — keep flowing by discarding, good for state-like streams where only the latest matters. - **`Channel.CONFLATED`** / `conflate()` — keep only the most recent value. ## Decision guide - Synchronous callback that cannot suspend -> **trySend** (+ a buffering/overflow policy). - You can suspend and want strict backpressure -> **send** from a launched coroutine. - Don't ignore the `ChannelResult` from `trySend` if dropping events is unacceptable. ## Key APIs `trySend`, `send`, `ChannelResult` (`isSuccess`/`isFailure`/`isClosed`), `buffer`, `BufferOverflow`, `conflate`, `ProducerScope`.

  • How would you call send from inside a synchronous callback?
    You can't directly; wrap it in launch { send(v) } using the ProducerScope's coroutine context, accepting the extra coroutine and ordering caveats — or just use trySend with a buffer.
  • What does a failed trySend mean and how do you react?
    It means no capacity (or closed). Inspect ChannelResult: if isClosed handle termination; otherwise the value was not delivered, so log/drop/conflate per your policy.

saying these in an interview costs you the question

  • Thinks trySend suspends or blocks the callback thread
  • Calls send directly in a non-suspending callback
  • Ignores trySend's ChannelResult and assumes delivery
  • Unaware the default channel can drop/fail under load
  • Confuses buffer() overflow policies

context