Brifing

Kotlin Flow's Hidden Channel Overhead: Surprising Fusion Breakers

Common operators like map() silently create extra coroutines — here's the proof.

ProAndroidDev - Medium2 min

1The Fusion Myth

Kotlin's coroutine library fuses adjacent buffer(), flowOn(), and produceIn() calls into a single channel through FusibleFlow interface. This isn't just theory — it's confirmed in the source code. However, this fusion breaks when certain 'harmless' operators are inserted between fusible calls, creating unexpected overhead. The article's author built instrumentation to measure this effect directly.

2Experimental Proof

Through reflection and coroutine debugging, the author proved that inserting a simple map() transform between buffer() and flowOn() creates: 1) An additional wrapper object in the Flow chain, 2) An extra producer coroutine at runtime, and 3) Three thread hops instead of one. This occurs despite identical functionality — the only difference being operator ordering.

3Unexpected Culprits

Testing revealed four common operators that break fusion: map(), catch(), onEach(), and distinctUntilChanged(). Surprisingly, conflate() maintains fusion because it's internally implemented as buffer(). This distinction matters because each broken fusion case creates separate channels and coroutines — invisible in code but measurable in performance.

4Runtime Impact

Coroutine debug dumps showed fused pipelines use just 2 coroutines (collector + producer), while broken chains require 3. Custom dispatcher logging proved thread hops triple from 1 to 3. These overheads compound in high-throughput scenarios like sensor pipelines or WebSocket feeds, where micro-optimizations matter most.

5The conflation Exception

conflate() behaves differently than other transforms because it's essentially buffer() with different parameters. When substituted for map() in tests, it maintained fusion and reduced coroutine count back to 2. This reveals implementation details matter more than operator appearance — what looks 'lightweight' may carry hidden costs.

6Practical Implications

The findings don't mean avoiding these operators — they're essential tools. Instead, developers should: 1) Group fusible operators together, 2) Place transforms either before or after fused blocks, not between them, and 3) Remember that 'simple-looking' operators aren't necessarily free in Flow pipelines.

7When Optimization Matters

For most UI Flows, these micro-optimizations won't be noticeable. But in high-frequency data streams (10,000+ events/second), each extra coroutine adds scheduling overhead. The article provides concrete measurement techniques to identify such bottlenecks using coroutine debugging and custom dispatchers.

8Beyond Folklore

This investigation proves that even well-documented behaviors like Flow fusion have undocumented edge cases. The methodology — combining source analysis, runtime instrumentation, and controlled experiments — provides a template for verifying other performance assumptions in coroutine-based systems.

Original article: ProAndroidDev - Medium — Kotlin Flow's Hidden Channel Overhead: Surprising Fusion Breakers