#282 Jul 26, 2026

282. mpsc::sync_channel — A Channel That Pushes Back When the Consumer Falls Behind

mpsc::channel() never says no: a fast producer and a slow consumer means the queue — and your memory — grows without bound. sync_channel(n) caps the queue and makes the producer wait.

Bite 281 used mpsc::channel() to collect results from workers. Its buffer is unbounded — send always succeeds immediately, no matter how far behind the receiver is:

1
2
3
4
5
6
7
8
use std::sync::mpsc;

let (tx, rx) = mpsc::channel();
for i in 0..1_000_000 {
    tx.send(i).unwrap(); // never blocks, just buffers
}
// receiver hasn't read a single message yet —
// all million are sitting in the queue

If the consumer parses files, writes to a database, or talks to a network, the producer can outrun it by orders of magnitude. Nothing crashes; the process just eats memory until something else does.

mpsc::sync_channel(n) creates a bounded channel: at most n messages queued. Once full, send blocks until the receiver catches up — that’s backpressure. try_send gives you the non-blocking version that reports the problem instead:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::sync::mpsc::{self, TrySendError};

let (tx, rx) = mpsc::sync_channel(2);

tx.send(1).unwrap();
tx.send(2).unwrap(); // buffer full

// send(3) would block here — try_send tells you why
assert!(matches!(
    tx.try_send(3),
    Err(TrySendError::Full(3))
));

assert_eq!(rx.recv().unwrap(), 1); // one slot frees up
tx.try_send(3).unwrap();           // now it fits

Note the error hands the value back — TrySendError::Full(3) — so nothing is lost; you can retry, drop it, or spill it elsewhere.

The odd-looking special case is sync_channel(0): a rendezvous channel with no buffer at all. Every send blocks until a recv is ready to take the value, so each handoff is also a synchronization point between the two threads:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use std::thread;

let (tx, rx) = mpsc::sync_channel(0);

let worker = thread::spawn(move || {
    tx.send("done").unwrap(); // blocks until main receives
    // reaching this line proves main got the message
});

assert_eq!(rx.recv().unwrap(), "done");
worker.join().unwrap();

Rule of thumb: channel() when producers must never wait, sync_channel(n) when the consumer sets the pace. If you’d rather drop data than slow the producer, try_send makes that an explicit decision instead of an accidental out-of-memory.

← Previous 281. mpsc::channel — Send Results Back Instead of Fighting Over a Mutex Next → 283. recv_timeout — Wait for a Message, But Not Forever