skip to content

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