Skip to content

Channels and Message Passing | Rust

Channels implement the actor model — concurrent tasks communicate by sending messages rather than Sharing memory. Rust provides several channel types, each optimized for different communication Patterns. The sender and receiver are separate endpoints; messages are moved from sender to Receiver, transferring ownership.

TypeProducersConsumersBufferingUse Case
std::sync::mpscMultipleSingleBounded/UnboundedSimple work distribution
tokio::sync::mpscMultipleSingleBounded/UnboundedAsync work distribution
oneshotSingleSingleNoneSingle response
broadcastSingleMultipleBoundedPub/sub notifications
watchSingleMultipleSingle valueConfiguration updates

The standard library”s channel is synchronous (blocking) and designed for OS threads:

use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
let val = String::from("hello");
tx.send(val).unwrap();
// val is moved — no longer accessible here
});
let received = rx.recv().unwrap();
assert_eq!(received, "hello");

Clone the sender to create multiple producers:

use std::sync::mpsc;
use std::thread;
let (tx, rx) = mpsc::channel();
let tx1 = tx.clone();
thread::spawn(move || {
tx.send("from thread 1").unwrap();
});
thread::spawn(move || {
tx1.send("from thread 2").unwrap();
});
drop(tx);
for received in rx {
println!("got: {}", received);
}

When all senders are dropped, recv() returns Err and the iterator terminates.

use std::sync::mpsc;
let (tx, rx) = mpsc::channel(); // unbounded — grows as needed
let (tx, rx) = mpsc::sync_channel(10); // bounded — capacity 10
MethodBlocking?Returns
tx.send(val)Yes (if bounded and full)Result<(), SendError<T>>
rx.recv()Yes (if empty and senders exist)Result<T, RecvError>
rx.try_recv()NoResult<T, TryRecvError>
rx.recv_timeout(dur)Yes (with timeout)Result<T, RecvTimeoutError>

Tokio’s async channel uses .await instead of blocking:

use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel(32);
tokio::spawn(async move {
tx.send("hello").await.unwrap();
});
while let Some(msg) = rx.recv().await {
println!("{}", msg);
}
}
use tokio::sync::mpsc;
let (tx, rx) = mpsc::channel(32); // bounded — capacity 32
let (tx, rx) = mpsc::unbounded_channel(); // unbounded — grows as needed

Channels are Rust’s answer to “share memory by communicating” instead of “communicate by sharing memory.” Think of a channel as a pipe: one end sends messages, the other receives them. The sender moves ownership of data into the pipe, so the receiver gets exclusive access. This eliminates the need for locks in many cases. Bounded channels are like a queue with a maximum length: if the queue is full, the sender blocks until space opens up. This creates natural backpressure, preventing one fast producer from overwhelming a slow consumer.