---
title: "22.2 Message passing: channels"
description: "Threads communicating by sending values through an mpsc channel, where send moves ownership and the compiler prevents use-after-send."
url: "https://learnrust.net/chapter-22/message-passing-channels/"
last_updated: "2026-06-13"
---

# 22.2 Message passing: channels

Threads that can't talk to each other aren't much use. There are two broad ways for threads to share information: pass *messages* between them, or share access to the *same memory*. This lesson covers the first, which a famous slogan from the Go language recommends as the default: "Do not communicate by sharing memory; instead, share memory by communicating." Rust's tool for it is the **channel**, and it fits the ownership system so naturally that the compiler eliminates a whole class of bugs for free.

## A channel has two ends

A channel is a one-way pipe between threads. It has a **transmitter** end (where you send values in) and a **receiver** end (where they come out). The standard library's `mpsc::channel` creates a connected pair. The name `mpsc` stands for "multiple producer, single consumer", many transmitters can feed one receiver, which we'll use shortly.

Here's one thread sending a value to another:

```rust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let message = String::from("hello from the thread");
        tx.send(message).unwrap();
    });

    let received = rx.recv().unwrap();
    println!("got: {received}");
}
```

```
got: hello from the thread
```

`mpsc::channel()` returns the pair `(tx, rx)`, transmitter and receiver, by convention. The spawned thread (a `move` closure, lesson [22.1](https://learnrust.net/chapter-22/threads/), so it owns `tx`) calls `tx.send(message)` to put a value into the channel. Meanwhile `main` calls `rx.recv()`, which **blocks**, waits, until a value arrives, then returns it. Both `send` and `recv` return `Result`: `send` fails if the receiver is gone, `recv` fails if all transmitters are gone, so there's nothing left to wait for. We `unwrap` both here.

## send moves ownership, and that's the magic

Here's the part that makes channels delightful in Rust specifically. When you `send` a value, you **give it away**: `send` takes ownership of the value and moves it down the channel (chapter 8). After sending, the sending thread no longer owns it and *cannot use it*. Try:

```rust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let message = String::from("hello");
        tx.send(message).unwrap();
        println!("sent: {message}");   // error: message was moved into send
    });

    println!("{}", rx.recv().unwrap());
}
```

```
error[E0382]: borrow of moved value: `message`
 --> src/main.rs:9:25
  |
7 |         let message = String::from("hello");
  |             ------- move occurs because `message` has type `String`, which does not implement the `Copy` trait
8 |         tx.send(message).unwrap();
  |                 ------- value moved here
9 |         println!("sent: {message}");
  |                         ^^^^^^^^^ value borrowed here after move
```

This is the same E0382 "use after move" you've seen since chapter 8, now doing concurrency-safety work. Think about the bug it's preventing. If the sending thread could keep using `message` after sending it, *both* threads would have access to the same `String` at the same time, the receiver and the sender, each free to read or modify it. That's a data race waiting to happen. But `send` moved ownership, so the sender gave the value up, and there's exactly one owner at all times. The receiver gets the value; the sender can't touch it. The ownership rule that prevented double frees in chapter 8 prevents shared-access races here, with no new machinery.

> **Key insight**
>
> Channels make concurrency safe by transferring *ownership*, not by sharing it. A value is owned by the sender, then by the channel, then by the receiver, never by two threads at once. So there's no shared mutable state to race over, and the chapter-8 move rules guarantee it. "Share memory by communicating" works in Rust because `send` is a move: the compiler proves, at compile time, that a sent value is never touched again by the sender.

## The receiver as an iterator

You'll usually send many values, not one. The receiver can be treated as an **iterator** (chapter 19): iterating it yields each value as it arrives and ends naturally when all transmitters have been dropped and no more values can come.

```rust
use std::sync::mpsc;
use std::thread;
use std::time::Duration;

fn main() {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        for s in ["one", "two", "three"] {
            tx.send(String::from(s)).unwrap();
            thread::sleep(Duration::from_millis(10));
        }
        // tx is dropped here when the closure ends
    });

    for received in rx {
        println!("got: {received}");
    }
}
```

```
got: one
got: two
got: three
```

The `for received in rx` loop pulls values out one at a time as the thread sends them, printing each. When the spawned thread finishes, its `tx` is dropped; with no transmitters left, the channel is closed, the iterator ends, and the loop exits. No "are we done?" flag, no counting, the channel closing *is* the end signal, exactly as `None` ends an iterator.

## Multiple producers

The "mp" in `mpsc` means you can have many senders feeding one receiver. Clone the transmitter (it's designed to be cloned), give each clone to a different thread, and all of them funnel into the same `rx`:

```rust
use std::sync::mpsc;
use std::thread;

fn main() {
    let (tx, rx) = mpsc::channel();

    for id in 1..=3 {
        let tx = tx.clone();        // each thread gets its own transmitter
        thread::spawn(move || {
            tx.send(format!("message from worker {id}")).unwrap();
        });
    }
    drop(tx);   // drop the original so the channel can close

    for received in rx {
        println!("{received}");
    }
}
```

```
message from worker 1
message from worker 3
message from worker 2
```

Each of the three threads sends one message through its own cloned `tx`; all three arrive at the single `rx`. The order is whatever the OS scheduling produces (here 1, 3, 2, yours may differ), the non-determinism from lesson 22.1. One subtlety: we `drop(tx)` (the original transmitter) after the loop, because the `for received in rx` loop only ends when *every* transmitter is gone. If the original `tx` stayed alive in `main`, the receiver would wait forever for a message that's never coming. Dropping it lets the channel close once the workers finish.

> **Best practice**
>
> Prefer message passing (channels) over shared state when threads can be organized as producers and consumers handing work or results to each other. It sidesteps the hardest concurrency bugs entirely, there's no shared mutable data to protect, because ownership moves with each message. Reach for shared state (next lesson) only when the problem genuinely requires multiple threads touching the *same* data, like a shared counter or cache, rather than passing values along.

## Quiz time

**Question #1**

What are the two ends of a channel called, and what does each do?

<details class="solution">
<summary>Show solution</summary>

The **transmitter** (`tx`) sends values into the channel with `tx.send(value)`; the **receiver** (`rx`) takes them out with `rx.recv()` (which blocks until a value arrives) or by iterating. `mpsc::channel()` returns the connected pair. "mpsc" means multiple producer, single consumer: you can clone the transmitter for many senders, but there's one receiver.

</details>

**Question #2**

Why can't the sending thread use a value after passing it to `tx.send(value)`?

<details class="solution">
<summary>Show solution</summary>

Because `send` takes ownership of the value and moves it into the channel (chapter 8). After the move the sender no longer owns it, so using it is a "use after move" compile error (E0382). This is exactly the safety guarantee: it ensures the value isn't accessible to both the sender and the receiver at once, which would be a data race. Ownership transfer makes shared access impossible.

</details>

**Question #3**

In the multiple-producer example, why is `drop(tx)` needed before the receiving loop?

<details class="solution">
<summary>Show solution</summary>

The `for received in rx` loop ends only when *all* transmitters have been dropped (so no more messages can arrive). The worker threads drop their cloned transmitters when they finish, but the original `tx` in `main` would stay alive, keeping the channel open and making the receiver wait forever for a message that never comes. `drop(tx)` releases that last transmitter so the channel can close once the workers are done.

</details>

Channels move data between threads so nothing is shared. But sometimes threads genuinely must share the *same* data, a counter, a cache, a configuration. The next lesson ([22.3](https://learnrust.net/chapter-22/shared-state-mutex-and-arc/)) covers that harder case with `Mutex` for safe access and `Arc` for shared ownership across threads.

## Sitemap

See the full [sitemap](https://learnrust.net/sitemap.md) for all pages.
