skip to content

Channel vs Flow

A Channel is hot and each value is consumed once, while a Flow is cold and every collector gets its own run. Interviewers ask which to expose from an API, and the usual answer is a Flow, with receiveAsFlow bridging an internal channel.

part ofKotlinoverview, primer and where to startread it →
on this pageshow

questions

5

What is the core difference between a Channel and a Flow in Kotlin coroutines, and what does it mean to call a Channel 'hot' and a Flow 'cold'?

level: juniorimportance: must knowfreq 70%

answer

  1. Flow = cold = re-runs per collect
  2. Channel = hot = lives without a receiver
  3. Channel element -> exactly one receiver
  4. receiveAsFlow / consumeAsFlow bridge
  5. Inter-coroutine handoff = Channel

basics

~20 s

A Channel is like a queue that passes items between coroutines; each item is taken by one receiver. A Flow is a recipe that re-runs from the start for every collector. Channel is hot (always live); Flow is cold (starts when collected).

solid answer

~40 s

A Flow is cold: nothing runs until a terminal operator like collect starts it, and each collect call re-executes the producer block from scratch. It carries no buffered state and a value goes to whoever is collecting. A Channel is hot: it exists and accepts send() calls independent of any receiver, buffering elements according to its capacity. Each element sent to a plain Channel is consumed by exactly one receiver (single-consumption / fan-out semantics), so multiple receivers split the stream rather than each getting a full copy. So you reach for Channel for inter-coroutine communication (one coroutine produces, another consumes), and Flow for declarative, replayable, multi-collector streams. receiveAsFlow / consumeAsFlow bridge the two.

code

kotlin · 7 lines
kotlin
val flow = flow { emit(1); emit(2) }   // cold: each collect re-emits 1,2
flow.collect(::println)                  // 1 2
flow.collect(::println)                  // 1 2 again

val channel = Channel<Int>()             // hot: lives independently
launch { channel.send(1); channel.send(2); channel.close() }
for (x in channel) println(x)            // each value received once

go deeper

for a junior

States the hot/cold distinction and that a Channel passes items between coroutines while a Flow is a replayable recipe.

for a middle

Adds single-consumption fan-out for Channel vs full-stream-per-collector for Flow and names receiveAsFlow/consumeAsFlow.

for a senior

Explains when to choose each, the lazy per-collector execution of Flow, and why SharedFlow/StateFlow exist for hot multicast.

for a principal

Frames it as a design choice (push handoff vs declarative pull), discusses backpressure/buffering implications and API surface (expose Flow, produce via Channel).

## The two abstractions **Flow** (`kotlinx.coroutines.flow.Flow`) is a *cold asynchronous stream*. "Cold" means the code that emits values does **nothing** until a **terminal operator** (most commonly `collect`) is called. Each separate `collect` runs the producer block **again from the beginning**, so two collectors each get their own independent run of all the values. **Channel** (`kotlinx.coroutines.channels.Channel`) is a *hot* primitive — essentially a coroutine-safe, suspending queue used to pass elements **between coroutines**. "Hot" means it is alive and accepts `send()` calls regardless of whether anyone is receiving. Elements may be buffered (depending on capacity) and sit there until received. ## Cold vs hot, precisely - **Cold (Flow):** producer is *lazy*, *per-collector*, and *replayable*. No collector ⇒ no work. Two collectors ⇒ two independent executions. - **Hot (Channel):** producer runs independently of consumers; state/elements live in the channel; values produced before a receiver attaches may be buffered or (if no room) suspend the sender. ## Single-consumption (the key Channel property) A plain `Channel` delivers **each element to exactly one receiver**. If two coroutines call `receive()`, the elements are **split** between them (fan-out), not duplicated. This is the opposite of `Flow`, where each collector sees the whole stream, and of `SharedFlow`, where each collector also sees a (shared) copy. ## Bridging a Channel into a Flow You often produce via a `Channel` (because production is push-based / inter-coroutine) but expose a `Flow` to callers: ```kotlin val channel = Channel<Int>() // receiveAsFlow: multiple collectors COMPETE for elements (single-consumption preserved) val flow1 = channel.receiveAsFlow() // consumeAsFlow: single collection only; cancels/closes the channel when collection ends val flow2 = channel.consumeAsFlow() ``` - `receiveAsFlow()` keeps single-consumption semantics: if collected by several collectors, they **compete** for elements; it does **not** close the channel. - `consumeAsFlow()` is **single-shot**: it cancels the channel after the collector finishes and throws if collected more than once. ## When to use which - Use a **Channel** for one-coroutine-to-another handoff, work queues, pipelines. - Use a **Flow** for declarative transformation chains, replayable cold sources, and APIs many collectors consume. - For hot, multi-collector broadcast state, prefer `StateFlow` / `SharedFlow` over a raw Channel.

  • If two coroutines collect the same plain Channel via receiveAsFlow, do both see every element?
    No. They compete; each element goes to exactly one of them (fan-out). Only Flow/SharedFlow give every collector the full stream.
  • Why does collecting a cold Flow twice run the producer twice?
    Because the Flow holds the producer lambda, not buffered results; collect simply invokes that lambda fresh each time.

Flow is a song sheet anyone can replay from the top; a Channel is a conveyor belt where each item is grabbed by one worker.

saying these in an interview costs you the question

  • Says a plain Channel broadcasts each element to all receivers
  • Claims a Flow buffers values and replays them by default
  • Thinks Flow emits without a terminal operator
  • Confuses Channel with SharedFlow/StateFlow
  • Says Channel and Flow are interchangeable with no semantic difference

context

open as a page

You expose a producer's output as receiveAsFlow() and two coroutines collect it concurrently. Each collector expects to see every element. Why is this broken, and how do you fix it?

level: middleimportance: should knowfreq 45%

basics

~20 s

A Channel gives each item to only one receiver, so the two collectors split the elements instead of both seeing them all. To give every collector the full stream, use a SharedFlow (or shareIn) instead of a raw channel.

open as a page

What is the difference between Channel.receiveAsFlow() and Channel.consumeAsFlow()? When would you pick each?

level: middleimportance: should knowfreq 55%

basics

~20 s

Both turn a Channel into a Flow. consumeAsFlow can be collected only once and closes the channel afterward. receiveAsFlow can be collected by several collectors that share/compete for elements and does not close the channel.

open as a page

A teammate is using a raw Channel to back a public API stream that several callers collect. What questions do you ask to decide whether a Channel, a cold Flow, or a SharedFlow/StateFlow is the right primitive?

level: seniorimportance: should knowfreq 35%

basics

~20 s

Ask: should every caller get every value or should values be split among them? Does the stream start fresh per caller or run once and broadcast? Is there a current value to read instantly? Those answers point to cold Flow, SharedFlow, StateFlow, or Channel.

open as a page

Explain the ownership and cleanup differences when you expose a Channel through consumeAsFlow() versus receiveAsFlow(), and how each affects resource leaks and cancellation propagation.

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

consumeAsFlow makes the Flow own the channel: when collection stops, the channel is cancelled, so resources are cleaned up automatically. receiveAsFlow does not own it, so you must close the channel yourself or collectors hang and resources leak.

open as a page