skip to content

How does flowOn affect buffering, concurrency, and emission ordering across the dispatcher boundary?

level: seniorimportance: should knowfreq 45%

answer

  1. flowOn implies a buffer() at the boundary
  2. Producer/consumer overlap = better throughput
  3. Order is still FIFO-preserved
  4. Not parallelism — single producer coroutine
  5. Fuses with adjacent flowOn/buffer/conflate

basics

~20 s

flowOn runs the producer on another dispatcher and passes items to the collector through a channel. This lets producer and collector run at the same time, but items still arrive in the order they were emitted.

solid answer

~40 s

Because flowOn moves the upstream to a different CoroutineContext, the upstream coroutine and the downstream collector can run concurrently. To bridge the boundary, flowOn introduces an internal buffered channel (default capacity), so it implicitly behaves like a buffer() between upstream and downstream — production overlaps with consumption, which can improve throughput. Crucially, ordering is preserved: items are delivered to the collector in emission order, because a single channel carries them sequentially. flowOn does NOT introduce parallelism within the upstream (one producer coroutine); for parallel processing you need flatMapMerge/operators or channelFlow. flowOn fuses with adjacent buffer/conflate operators and with other flowOn calls to avoid redundant channels. You can tune the implied buffer by combining with buffer(capacity) or conflate downstream.

code

kotlin · 17 lines
kotlin
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

suspend fun main() {
    flow {
        for (i in 1..3) {
            // pretend this is heavy work
            emit(i)
        }
    }
        .flowOn(Dispatchers.Default)  // upstream runs concurrently + buffered
        .collect { value ->
            // collector can be slow; upstream already produced ahead
            println("got $value in order")
        }
    // prints 1,2,3 in order despite concurrency
}

go deeper

for a junior

Knows flowOn changes the producer's thread.

for a middle

Understands flowOn lets producer and collector overlap and keeps order.

for a senior

Explains the implicit buffer, FIFO ordering, lack of parallelism, and tuning via buffer/conflate.

for a principal

Reasons about operator fusion, throughput/latency tradeoffs, and when to reach for channelFlow/flatMapMerge instead.

## The concurrency this creates Without `flowOn`, producer and collector are the **same** coroutine running sequentially: emit → handle → emit → handle. With `flowOn(dispatcher)`, the upstream runs in its **own** coroutine on another dispatcher. Now the producer can be working on the next item while the collector processes the previous one — **producer/consumer overlap**. ## The implicit buffer To hand items across the context boundary, `flowOn` uses an internal **buffered channel**. So `flowOn` inherently provides buffering similar to `buffer()`: ```kotlin flow { repeat(3) { emit(produce(it)) } // can run ahead }.flowOn(Dispatchers.Default) .collect { slowConsume(it) } // consumes while producer is ahead ``` Production and consumption overlap; the channel holds in-flight items up to its capacity. ## Ordering is preserved Even though two coroutines run concurrently, **emission order is preserved**. A single channel carries items FIFO, and there is exactly one upstream producer coroutine, so the collector sees items in the exact order they were emitted. `flowOn` is **not** a parallelism operator. ## What flowOn does NOT do - It does not run upstream operators in parallel with each other. One sequential producer. - For real parallelism use `flatMapMerge`, `channelFlow { launch { send(...) } }`, or split work explicitly. ## Fusion / optimization The Flow runtime **fuses** adjacent context-changing/buffering operators: consecutive `flowOn` calls and adjacent `buffer`/`conflate` are merged so you don't get a redundant chain of channels. You can tune behavior with downstream `buffer(n)` (larger queue), `conflate()` (drop intermediates, keep latest), or `buffer(onBufferOverflow = ...)`. ```kotlin flow { ... } .flowOn(Dispatchers.IO) // implicit buffer here .conflate() // collector only keeps latest while busy .collect { render(it) } ``` ## Key APIs/keywords - `flowOn`, internal buffered `Channel` - `buffer(capacity, onBufferOverflow)`, `conflate()` - `flatMapMerge`, `channelFlow` (for actual parallelism) - operator fusion of adjacent context/buffer operators

  • Does flowOn parallelize upstream operators?
    No. It moves the upstream to another dispatcher as a single sequential producer. Use flatMapMerge or channelFlow for parallelism.
  • How do you change the implicit buffer size or overflow behavior?
    Add buffer(capacity, onBufferOverflow) or conflate() to the chain; the runtime fuses it with flowOn's channel.

flowOn is a conveyor belt between a cook and a waiter in separate rooms: they work at once and the belt keeps the dishes in order.

saying these in an interview costs you the question

  • Claiming flowOn runs upstream operators in parallel
  • Saying flowOn can reorder emissions
  • Not realizing flowOn adds an implicit buffer
  • Thinking buffer/conflate after flowOn creates a second redundant channel (it fuses)

context