skip to content

What does it mean that a Kotlin Flow is "cold", and what does that imply about when its code runs?

level: juniorimportance: must knowfreq 80%

answer

  1. Nothing runs until collect
  2. flow {} = recipe, not execution
  3. Terminal operator triggers it
  4. Each collect re-runs from the top
  5. Cold vs hot (StateFlow/SharedFlow)

basics

~10 s

A cold Flow does nothing on its own. The code inside it runs only when you collect it. Until someone collects, no values are produced and no work happens.

solid answer

~40 s

A Flow is cold because the block passed to the flow {} builder is not executed when the Flow is created — it runs lazily, only when a terminal operator such as collect is called. Creating a Flow just builds a description (a recipe) of how to produce values; it is inert. Each call to collect starts the producer block from the beginning, so the flow is re-runnable. Contrast this with a hot source like a SharedFlow or StateFlow, which emits regardless of collectors. Cold flows are also sequential per collector: emit suspends until the collector finishes processing the previous value. This laziness means side effects (network calls, logging) inside flow {} are deferred until collection, which is the key mental model for reasoning about Flow.

code

kotlin · 7 lines
kotlin
fun numbers(): Flow<Int> = flow {
    println("producing")
    for (i in 1..3) { emit(i); delay(50) }
}

val f = numbers()      // prints nothing
f.collect { print(it) } // prints "producing" then 123

go deeper

for a junior

Knows nothing runs until collect and that flow {} is a deferred recipe.

for a middle

Distinguishes intermediate vs terminal operators and explains re-execution per collect.

for a senior

Contrasts cold vs hot, relates laziness to deferred side effects and resource management.

for a principal

Frames coldness as a design choice for composability and backpressure, and discusses where hot conversions belong in an architecture.

## What "cold" means In Kotlin coroutines, a `Flow<T>` is a **cold** asynchronous data stream. "Cold" means the **producer code does not run until the Flow is collected**. Creating a Flow with the `flow { ... }` builder, `flowOf(...)`, or `asFlow()` only constructs an object that *describes* how to produce values — it does not start producing them. - **Terminal operator** = an operator that actually triggers execution and consumes the stream, such as `collect`, `toList`, `first`, `single`, `reduce`, `fold`. It is `suspend` and must run inside a coroutine. - **Intermediate operator** = `map`, `filter`, `onEach`, `transform`, etc. These are also lazy: they just wrap the upstream Flow and return a new cold Flow. They run only when a terminal operator pulls values through. ## Why this matters ```kotlin fun fetchUsers(): Flow<User> = flow { println("start") // does NOT print here emit(api.getUser(1)) } val f = fetchUsers() // nothing prints, no network call f.collect { println(it) } // NOW "start" prints and the API is called ``` Because the block is deferred, **side effects are deferred too**. Logging, network requests, and database reads inside `flow {}` happen at collection time, not at construction time. ## Re-runnable on each collect Every call to `collect` runs the producer block **again, from the top**, independently. Two collectors get two separate executions: ```kotlin val ticks = flow { var i = 0 while (true) { emit(i++); delay(100) } } ticks.collect { ... } // its own counter starting at 0 ticks.collect { ... } // a fresh, independent counter starting at 0 ``` ## Sequential within one collector Inside a single collection, `emit` and the collector body run **sequentially**: `emit` suspends the producer until the collector has finished handling the current value, then resumes to produce the next. There is no concurrency between producer and consumer in a plain Flow. ## Cold vs hot - **Cold** (`flow {}`, `flowOf`, `asFlow`): per-collector execution, lazy, starts on collect. - **Hot** (`StateFlow`, `SharedFlow`, channels): exist and may emit independently of collectors; collectors share one running source. The practical takeaway: a Flow is a **lazy recipe**, not a running stream. Nothing executes until `collect`.

  • If you call map on a Flow but never collect it, does the map lambda run?
    No. map is an intermediate operator; it returns a new cold Flow. Its lambda only runs when a terminal operator collects the chain.
  • Name a terminal operator other than collect.
    toList, first, single, reduce, fold, or count — each starts the upstream and consumes values.

A cold Flow is like a recipe card: writing it down cooks nothing — only following it (collecting) produces the meal.

saying these in an interview costs you the question

  • Saying the flow {} block runs as soon as the Flow is created
  • Thinking map/filter execute immediately without a terminal operator
  • Confusing Flow with StateFlow (treating it as always-emitting)
  • Believing a Flow caches values from a previous collect

context