Networking with the Standard Library

Read and Write Timeouts

Read and write timeouts prevent blocking socket operations from hanging indefinitely when a remote peer crashes silently, a firewall drops packets without sending a TCP RST, or a network link becomes congested. Both TcpStream and UdpSocket configure socket-level timeouts using Option<std::time::Duration>.

Timeout Configuration Methods

Method Signature Description
.set_read_timeout(dur) set_read_timeout(&self, dur: Option<Duration>) -> io::Result<()> Sets the maximum wait time for a single read/recv call (None disables the timeout).
.set_write_timeout(dur) set_write_timeout(&self, dur: Option<Duration>) -> io::Result<()> Sets the maximum wait time for a single write/send call (None disables the timeout).
.read_timeout() read_timeout(&self) -> io::Result<Option<Duration>> Queries the currently configured read timeout.
.write_timeout() write_timeout(&self) -> io::Result<Option<Duration>> Queries the currently configured write timeout.
TcpStream::connect_timeout(&addr, dur) connect_timeout(addr: &SocketAddr, timeout: Duration) -> io::Result<TcpStream> Bounds the initial TCP handshake duration.

Passing Some(Duration::ZERO) to set_read_timeout or set_write_timeout is rejected at runtime with Err(io::ErrorKind::InvalidInput).

Handling Timeout Errors Across Platforms

When a read or write timeout expires on a blocking socket, the returned io::Error has a kind of either ErrorKind::WouldBlock (common on Unix where SO_RCVTIMEO returns EAGAIN/EWOULDBLOCK) or ErrorKind::TimedOut (common on Windows where WSAETIMEDOUT is returned). Always match both ErrorKind::WouldBlock | ErrorKind::TimedOut when checking for socket timeouts:

use std::io::{self, ErrorKind, Read};
use std::net::{TcpListener, TcpStream};
use std::time::Duration;

fn is_timeout(err: &io::Error) -> bool {
    matches!(err.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut)
}

fn main() -> io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:0")?;
    let addr = listener.local_addr()?;

    let mut client = TcpStream::connect_timeout(&addr, Duration::from_secs(1))?;
    let (_server_idle, _) = listener.accept()?;

    // Configure a 25ms read timeout on the client
    let timeout = Duration::from_millis(25);
    client.set_read_timeout(Some(timeout))?;
    assert_eq!(client.read_timeout()?, Some(timeout));

    // `_server_idle` stays open without sending any bytes, triggering a read timeout
    let mut buf = [0u8; 32];
    match client.read(&mut buf) {
        Ok(n) => println!("Unexpectedly read {n} bytes"),
        Err(ref e) if is_timeout(e) => {
            println!("Read timed out cleanly after 25ms");
        }
        Err(e) => return Err(e),
    }

    // Clear the read timeout by passing `None`
    client.set_read_timeout(None)?;
    assert_eq!(client.read_timeout()?, None);

    Ok(())
}
Read timed out cleanly after 25ms

Per-Syscall Timeout vs Total Operation Deadline

set_read_timeout and set_write_timeout apply to each individual underlying OS read or write system call, not to composite methods like read_to_end, read_exact, or write_all. If an attacker trickles 1 byte every 9 seconds against a 10-second set_read_timeout while the server calls read_to_end, the connection can stay open indefinitely (a "Slowloris" attack). Enforce an overall wall-clock Instant deadline across multi-read loops to defend against trickle attacks.