Channel: what it is, channel types and coroutines in Kotlin

Author: IT Sectr Published: 2026-03-17 Reading time: 8 min

Channel is a synchronization primitive from the Kotlin Coroutines library for passing data between coroutines. According to Kotlin Documentation, 2025, Channel implements the producer-consumer pattern with blocking transmission through suspend functions. Channel supports Rendezvous, Buffered and Conflated modes, each of which determines behavior when overflowing.

Key Takeaways

  • Channel is a data transfer primitive between coroutines from kotlinx.coroutines, based on the producer-consumer pattern
  • Rendezvous Channel — no buffer: send() suspends until receive() is called
  • Buffered Channel — with a buffer of specified capacity, send() suspends when full
  • Conflated Channel — stores only the latest value, old values are dropped on overflow
  • Channel is the foundation for building hot streams, callbackFlow and actor models

What is Channel in Kotlin?

Channel is conceptually similar to BlockingQueue from Java, but with suspend functions send() and receive() instead of blocking put() and take(). A Kotlin developer uses Channel to organize data exchange between coroutines without synchronization through shared memory. The channel guarantees ordered delivery — the order of sending matches the order of receiving.

Creating a Channel

To create a Channel the factory function Channel<T>(capacity) is called. The capacity parameter determines the channel type: RENDEZVOUS (0), UNLIMITED (Int.MAX_VALUE), CONFLATED (-1) or a specific number. The element type T is specified via generics. Closing the channel via close() signals that no new elements will come.

Send and Receive

send(value) is a suspend function that suspends the sending coroutine if the channel is full. receive() is a suspend function that suspends the receiver if the channel is empty. The alternatives trySend() and tryReceive() are non-blocking versions that return Boolean or null when the operation is not possible. They are useful in non-suspend contexts.

Channel Types in kotlinx.coroutines

Kotlin provides four Channel variants through buffer capacity: Rendezvous (capacity 0), Buffered (capacity N), Conflated (capacity 1, overwrite) and Unlimited (capacity Int.MAX_VALUE). Each type solves its own task, from strict synchronization to mass data buffering.

Rendezvous Channel is the strictest: send() blocks until receive() is called in another coroutine. In essence, this is a rendezvous point of two coroutines. Ideal for strict handshake when the sender must wait for the receiver to process the element. Data loss is ruled out — send does not complete until receive is performed.

Conflated Channel stores only the last sent value. If the sender puts a new element before the receiver picks up the old one, the old one is discarded. Conflated Channel is useful for UI state: if a user quickly changes the slider, intermediate values can be dropped and only the last one processed.

Producer-Consumer with Channels

The classic Producer-Consumer pattern on Channel is implemented through parallel coroutines. Producer calls send(value) in a loop, consumer calls receive(value). The producer and consumer can work on different Dispatchers: producer on Dispatchers.IO, consumer on Dispatchers.Main. Channel automatically synchronizes access without Lock or synchronized.

Fan-out — multiple consumers on a single channel. Each element goes to exactly one consumer (round-robin distribution). Fan-in — multiple producers write to a single channel. Sending coroutines compete for sending, but the order of elements is preserved. Both scenarios require no additional synchronization.

Produce is a coroutine builder that creates a channel with automatic closing. The produce { } function returns a ReceiveChannel — a read-only channel for the consumer. Inside the builder send() sends data, and when the block completes or an exception occurs the channel is automatically closed, preventing leaks.

Select and Multiplexing

The kotlinx.coroutines library provides select — an expression that awaits the first completed channel among several alternatives. Select allows multiplexing multiple channels: for example, awaiting data from two sources and processing the one that responded first. Syntax — select<T> { channel1.onReceive { } channel2.onReceive { } }. This is an alternative to the amb operator in Rx.

Channel Code Examples

The first example is a simple Rendezvous Channel where the sender waits for receive:

kotlin
val channel = Channel<String>()

scope.launch {
    channel.send("Hello")
    println("Sent")
}

scope.launch {
    val msg = channel.receive()
    println("Received: $msg")
}

The second example — multiple consumers on a single channel (fan-out):

kotlin
val channel = Channel<Int>(Channel.UNLIMITED)

scope.launch {
    for (x in 1..10) channel.send(x)
    channel.close()
}

repeat(2) { id ->
    scope.launch {
        for (msg in channel) {
            println("Consumer #$id: $msg")
        }
    }
}

The third example — using the produce builder with error handling:

kotlin
val source = produce {
    for (i in 1..5) {
        delay(200)
        send(i)
    }
}

scope.launch {
    source
        .consumeAsFlow()
        .catch { println("Error: $it") }
        .collect { println("Element: $it") }
}

Channel vs Flow

Channel is a hot primitive: data is emitted independently of subscribers. Flow is cold: data is generated upon subscription. Channel supports multiple producers and consumers with guaranteed delivery of each element to one consumer (fan-out). Flow is not designed for multiple independent producers.

Channel uses a buffer with configurable capacity and suspend functions send/receive for backpressure management. Flow uses the suspend mechanism collect with automatic backpressure through coroutines. Channel is a low-level tool for specific scenarios: callback conversion, actor model, task queue with multiple senders.

For everyday scenarios in Android (UI state, reactive streams from DB) Google recommends Flow rather than Channel. Channel should be used when hot data exchange between coroutines with precise buffer control is needed, or when converting callback interfaces via callbackFlow, whose internal implementation uses Channel.

An important practical example: when implementing a WebSocket client, Channel allows writing messages from one coroutine and reading from another with the guarantee that each message will be processed exactly once. Flow is not suitable for this task because it is cold and does not support multiple producers. Channel with UNLIMITED capacity ensures that incoming messages are not lost during temporary consumer delays.

Channel lifecycle management is an important part of working with Channel. The channel must be closed when all data has been sent so that the consumer can finish iteration. Calling channel.close() signals that no new elements will come. The consumer can iterate via for (item in channel) — the loop will finish automatically after close() and buffer depletion. Alternatively the consumer can call receive() in a loop with handling of ClosedReceiveChannelException.

Channel is actively used in Android for implementing EventBus without dependencies: a global Channel<Event> with a Broadcast strategy allows sending events from any point in the application. Unlike LiveData-based buses, Channel is not tied to lifecycle and does not require clearing when transitioning between screens. send() from ViewModel and receive() in Activity/Fragment via lifecycleScope provide type-safe communication without Event classes. Multiple consumers on a Channel distribute the load — each element is processed once, which prevents duplicate handling of a single event in different subscribers.

In actor systems Channel serves as the foundation for implementing a mailbox — a message queue for the actor. An actor is a coroutine that reads messages from a Channel in a loop and processes them sequentially. This approach guarantees that each message is processed in the order of sending, without data races. Kotlin does not have a built-in actor as a type (unlike Akka), but Channel + launch is a lightweight replacement.

For bidirectional exchange channel pairs are used: one channel for requests from client to server, the second for responses from server to client. For example, when implementing a Pipe in a multithreaded application: producer writes to OutputChannel, consumer reads from InputChannel. The suspend functions send and receive guarantee that the Producer-Consumer will not overflow the call stack, since coroutines suspend rather than block. Channel with BUFFERED capacity is suitable for most scenarios where producer and consumer speeds are roughly equal. For asymmetric scenarios use UNLIMITED so that the producer does not suspend when the consumer is busy — this reduces the risk of deadlock but increases memory consumption.

Choosing Channel Capacity

When designing an architecture with channels it is important to remember capacity: the choice of capacity directly affects behavior under peak load. Channels with BUFFERED(N) capacity act as a smoothing buffer: if the consumer is temporarily slower than the producer, elements accumulate. If the consumer's average speed is consistently lower than the producer's, the buffer will fill up and the sender coroutine will suspend — this is automatic backpressure protecting against memory overload.

For monitoring and debugging Channel use kotlinx-coroutines-debug: the utility shows the number of active coroutines, their channel state (open/closed, number of elements in the buffer), and the call stack of suspended send/receive operations. Channel can also be wrapped in a logging proxy: the LoggingChannel<T> class delegates calls to the real Channel, logging send, receive and close operations. This helps identify channel leaks when close() was not called and the consumer coroutine is waiting forever for new elements.

Frequently Asked Questions

How is Channel different from BlockingQueue?

Channel uses suspend functions send() and receive() instead of blocking put() and take(). Unlike BlockingQueue, Channel does not block the thread when overflowing — the coroutine suspends, freeing the thread for other coroutines. This is critical for efficient thread usage in Kotlin.

What happens when send() is called on a closed Channel?

When send() is called on a closed channel a ClosedSendChannelException is thrown. Before sending check isClosedForSend or use trySend() which returns false when closed. close() guarantees that already sent elements will be received before the exception is thrown.

When to use Conflated Channel?

Conflated Channel is useful for events where only the latest state matters — progress bar, slider position, touch coordinates. If the consumer cannot process all events, the intermediate ones are dropped and the latest one is guaranteed to be processed. Conflated Channel has capacity=-1.

How to close a channel and process remaining elements?

Call channel.close() — the channel is marked as closed for sending, but already sent elements continue to be read via receive(). Iteration with for (item in channel) finishes automatically after buffer depletion. isClosedForSend returns true immediately, isClosedForReceive returns true after depletion.

Can Channel be replaced with Flow?

Not always. Flow is cold — one emission per one collect. If multiple independent producers writing to a single stream are needed, Channel is mandatory. For simple data transfer between two coroutines use Channel. For reactive data streams use Flow.

Summary

  • Channel is a hot synchronization primitive for transferring data between coroutines
  • Rendezvous — no buffer, send blocks until receive is called
  • Buffered — with a buffer of specified capacity, send suspends when full
  • Conflated — stores only the last value, intermediate ones are discarded
  • Produce — coroutine builder for a channel with automatic closing
  • Fan-out — multiple consumers distribute elements via round-robin
  • For UI state use StateFlow, Channel is for hot queues and callback conversion

We will develop a mobile application turnkey

IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.

Discuss the project

Read also