How does flowOn affect buffering, concurrency, and emission ordering across the dispatcher boundary?
answer
- flowOn implies a buffer() at the boundary
- Producer/consumer overlap = better throughput
- Order is still FIFO-preserved
- Not parallelism — single producer coroutine
- Fuses with adjacent flowOn/buffer/conflate
basics
~20 sflowOn 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 sBecause 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 linesimport 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
Knows flowOn changes the producer's thread.
Understands flowOn lets producer and collector overlap and keeps order.
Explains the implicit buffer, FIFO ordering, lack of parallelism, and tuning via buffer/conflate.
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)