skip to content

What is a Channel<T> in Kotlin coroutines, and how do producer and consumer coroutines communicate through it?

level: juniorimportance: must knowfreq 70%

answer

  1. send/receive are suspend
  2. hot, not cold like Flow
  3. RENDEZVOUS = no buffer, direct handover
  4. for (x in channel) iterates receiver
  5. close() ends the loop

basics

~10 s

A Channel is a pipe between coroutines. One coroutine puts values in with send, another takes them out with receive. Both suspend (wait politely) until the other side is ready.

solid answer

~40 s

Channel<T> is a coroutine-safe queue used to pass a stream of values between coroutines. A producer calls suspend fun send(value), a consumer calls suspend fun receive(). Both are suspending functions: with the default RENDEZVOUS capacity, send suspends until a receiver is ready and receive suspends until a value is available, so the channel hands values over directly with no buffering. Unlike Flow (cold), a Channel is hot: values flow as soon as both ends meet, regardless of whether anyone is collecting yet. You create one with Channel<Int>() and typically iterate the consumer side with for (x in channel) or channel.consumeEach { }. When the producer is done it calls close(), which lets the consumer loop end cleanly.

code

kotlin · 6 lines
kotlin
val channel = Channel<Int>()
launch {
    for (x in 1..3) channel.send(x)
    channel.close()
}
for (y in channel) println(y)  // 1 2 3

go deeper

for a junior

Knows send/receive exist, that they suspend, and that close ends the loop.

for a middle

Explains hot vs cold vs Flow and the rendezvous handover precisely.

for a senior

Discusses when a Channel is the right tool vs a Flow and the resource/ownership implications.

for a principal

Frames Channel as the low-level communication primitive underlying produce/actor and shared-flow design tradeoffs.

## What a Channel is A `Channel<T>` is a concurrency primitive in `kotlinx.coroutines` for passing a *stream* of values of type `T` between coroutines. Think of it as a coroutine-aware `BlockingQueue`: instead of blocking a thread, its operations **suspend** the calling coroutine, freeing the thread to do other work. ## The two core operations - `suspend fun send(element: T)` — a **producer** offers a value into the channel. - `suspend fun receive(): T` — a **consumer** takes the next value out. Both are `suspend` functions, so they can only be called from a coroutine or another suspend function. "Suspend" means the coroutine pauses without blocking the underlying thread and resumes later when the operation can proceed. ## Hot vs cold A Channel is a **hot** stream: it exists and conducts values independently of consumers. This contrasts with `Flow`, which is **cold** (the producing code reruns for each collector). A Channel models *communication between coroutines*; a Flow models a *cold declarative pipeline*. ## Rendezvous by default With no capacity argument, `Channel<T>()` uses `RENDEZVOUS`: there is no buffer. `send` suspends until some coroutine calls `receive`, and vice versa. The value is handed over directly — a meeting point ("rendezvous"). ## Iterating the consumer side The channel implements `ReceiveChannel<T>`, which is iterable in a coroutine: ```kotlin import kotlinx.coroutines.* import kotlinx.coroutines.channels.* fun main() = runBlocking { val channel = Channel<Int>() launch { // producer coroutine for (x in 1..3) channel.send(x * x) channel.close() // signal: no more values } for (y in channel) { // suspends until each value / close println(y) // 1, 4, 9 } } ``` The `for (y in channel)` loop calls `receive` under the hood and ends when the channel is **closed and drained**. ## Closing `close()` marks the channel as done. Pending buffered values are still delivered; afterwards the consumer loop terminates. Calling `send` on a closed channel throws `ClosedSendChannelException`. ## Why use one Channels are the building block for producer/consumer pipelines, fan-out/fan-in work distribution, and the higher-level `produce { }` and `actor { }` builders.

  • How is a Channel different from a Flow?
    Channel is hot and is a communication primitive between coroutines; Flow is cold and reruns its producer per collector. Channels have explicit send/receive; Flow has emit/collect.
  • What happens to receive when the channel is closed and empty?
    receive() throws ClosedReceiveChannelException; the for-loop and consumeEach handle this for you and just exit.

Like passing a baton in a relay: the runner (sender) waits with the baton until the next runner (receiver) is there to take it.

saying these in an interview costs you the question

  • Saying a Channel is cold or reruns per consumer
  • Claiming send/receive block the thread instead of suspending
  • Thinking you can call send/receive from a normal (non-suspend) function
  • Forgetting to close() and wondering why the consumer loop hangs forever

context