Blocking Network IO
By default, all socket operations in std::net (TcpListener::accept, TcpStream::read, TcpStream::write, and UdpSocket::recv_from) perform blocking network I/O: when an operation cannot complete immediately, the OS kernel puts the calling thread to sleep until data arrives or buffer space becomes available.
Blocking Mode (Default):
Thread calls stream.read(&mut buf)
|
v
[Kernel Socket Receive Buffer Empty] ---> Thread sleeps (0% CPU)
|
[Packet Arrives from Network] ---> Kernel wakes thread -> returns Ok(n)
Non-Blocking Mode (.set_nonblocking(true)):
Thread calls stream.read(&mut buf)
|
v
[Kernel Socket Receive Buffer Empty] ---> Returns immediately:
Err(io::ErrorKind::WouldBlock)
Thread-Per-Connection Trade-Offs
In blocking mode, a thread blocked inside stream.read() cannot service any other connection until that read() completes.
| Concurrency Scale | Blocking I/O (std::net + OS Threads) |
Non-Blocking Event Loop (epoll/kqueue/IOCP) |
|---|---|---|
| 1 to ~500 concurrent sockets | Simple, sequential control flow; low overhead | Unnecessary complexity unless latency-critical |
| 10,000+ concurrent sockets | High memory cost (~2 MiB stack per thread) and context-switch overhead | Scales with a small fixed thread pool (e.g., mio, tokio) |
Switching Sockets to Non-Blocking Mode
TcpListener, TcpStream, and UdpSocket all provide .set_nonblocking(bool) -> io::Result<()>. When set to true, any operation that would have waited for network activity returns Err with io::ErrorKind::WouldBlock immediately:
use std::io::{self, ErrorKind, Read, Write};
use std::net::{TcpListener, TcpStream};
fn main() -> io::Result<()> {
let listener = TcpListener::bind("127.0.0.1:0")?;
let addr = listener.local_addr()?;
// Switch listener to non-blocking mode
listener.set_nonblocking(true)?;
// No client has connected yet, so accept() immediately returns WouldBlock
match listener.accept() {
Err(e) if e.kind() == ErrorKind::WouldBlock => {
println!("listener.accept() returned WouldBlock as expected");
}
other => panic!("unexpected result: {other:?}"),
}
// Connect a client and accept it
let mut client = TcpStream::connect(addr)?;
let (mut server_stream, _) = listener.accept()?;
server_stream.set_nonblocking(true)?;
// Client hasn't sent data yet -> server read returns WouldBlock
let mut buf = [0u8; 16];
assert_eq!(
server_stream.read(&mut buf).unwrap_err().kind(),
ErrorKind::WouldBlock
);
// Client sends 5 bytes -> server read succeeds immediately
client.write_all(b"hello")?;
let n = server_stream.read(&mut buf)?;
println!(
"Non-blocking read after write got: {:?}",
std::str::from_utf8(&buf[..n]).unwrap()
);
Ok(())
}
listener.accept() returned WouldBlock as expected
Non-blocking read after write got: "hello"
Avoiding Busy-Wait Spin Loops
In production code, never poll a non-blocking socket in a tight loop { match stream.read(...) { Err(WouldBlock) => continue, ... } } without sleeping or waiting on an OS readiness notification, as a spin loop pegs a CPU core at 100%. For single-socket bounded waiting in standard-library code, prefer set_read_timeout and set_write_timeout on a blocking socket.