Networking#

Rust’s standard library provides synchronous TCP / UDP and DNS, but most real applications use the async ecosystem on top of Tokio.

Synchronous (std)#

The stdlib’s std::net module wraps blocking BSD sockets. TcpStream::connect opens client connections; TcpListener::bind accepts servers; one thread per connection is the simple concurrency story. Fine for tools and tests; production workloads almost always reach for async.

use std::io::{Read, Write};
use std::net::TcpStream;

let mut s = TcpStream::connect("example.com:80")?;
s.write_all(b"GET / HTTP/1.0\r\nHost: example.com\r\n\r\n")?;
let mut buf = String::new();
s.read_to_string(&mut buf)?;

A blocking TCP server.

use std::net::TcpListener;

for stream in TcpListener::bind("0.0.0.0:9000")?.incoming() {
    std::thread::spawn(move || handle(stream?));
}

Async (Tokio)#

The de-facto async runtime. tokio::net mirrors std::net.

use tokio::net::TcpListener;
use tokio::io::{AsyncReadExt, AsyncWriteExt};

#[tokio::main]
async fn main() -> std::io::Result<()> {
    let listener = TcpListener::bind("0.0.0.0:9000").await?;
    loop {
        let (mut socket, _) = listener.accept().await?;
        tokio::spawn(async move {
            let mut buf = [0u8; 1024];
            loop {
                match socket.read(&mut buf).await {
                    Ok(0) => return,
                    Ok(n) => { let _ = socket.write_all(&buf[..n]).await; }
                    Err(_) => return,
                }
            }
        });
    }
}

HTTP Client#

The HTTP client crates an operator picks among. reqwest is the everyday choice with batteries included; ureq is blocking-only with a smaller dependency tree; hyper is the low-level building block. Build the client once with explicit timeouts and reuse it across requests.

  • reqwest , the everyday HTTP client.

  • ureq , blocking, no-async, smaller dependency tree.

  • hyper , low-level HTTP/1.1 + HTTP/2.

use reqwest::Client;
use std::time::Duration;

let client = Client::builder()
    .timeout(Duration::from_secs(5))
    .pool_max_idle_per_host(10)
    .build()?;

let user: User = client
    .get("https://api.example.com/users/1")
    .bearer_auth(token)
    .send().await?
    .error_for_status()?
    .json().await?;

HTTP Server#

The HTTP server crates. axum is the modern Tokio-native default with ergonomic routing; actix-web is the mature fast alternative with its own actor model; hyper is the low-level building block both wrap. axum has won most new-project decisions in 2026.

  • axum , ergonomic, modular, Tokio-native.

  • actix-web , mature, fast.

  • hyper , low-level building block.

  • See Frameworks for the broader picture.

use axum::{Router, routing::get};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/healthz", get(|| async { "ok" }));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

TLS / mTLS#

The TLS crates an operator picks among. rustls is the pure-Rust modern default; tokio-rustls wraps it for async code; native-tls delegates to the platform’s TLS stack (SChannel, Secure Transport, OpenSSL). Pick one and pin the feature flag in production builds.

  • rustls , pure-Rust TLS, the modern default.

  • tokio-rustls , async wrapper.

  • native-tls , platform TLS (SChannel / Secure Transport / OpenSSL).

reqwest uses rustls or native-tls depending on the feature flags. For client TLS in custom code.

use rustls::ClientConfig;
use rustls::pki_types::ServerName;

let cfg = ClientConfig::builder()
    .with_root_certificates(rustls::RootCertStore::empty())
    .with_no_client_auth();

WebSockets#

gRPC#

tonic is the standard gRPC implementation in Rust, sitting on top of hyper and tokio. tonic-build generates Rust types from .proto files at build time via a build.rs script, producing typed client and server stubs that integrate cleanly with the async runtime.

use tonic::Request;
use my_service::my_service_client::MyServiceClient;

let mut client = MyServiceClient::connect("http://host:50051").await?;
let resp = client.get_user(Request::new(GetUserReq { id: 1 })).await?;

tonic generates Rust types from .proto files via tonic-build in a build.rs.

DNS#

  • std::net::ToSocketAddrs, the system resolver, blocking.

  • tokio::net::lookup_host, async wrapper around the system resolver.

  • hickory-resolver (formerly trust-dns), pure-Rust DNS resolver and server.

Lower-Level#

  • socket2, raw socket option control.

  • mio, the event loop under Tokio.

Pitfalls#

The traps that catch Rust networking authors. reqwest’s default client has no timeout; TLS feature flags need explicit pinning; CPU-bound or blocking work inside an async task starves the Tokio runtime; single-threaded versus multi-threaded runtime choice has real performance implications.

  • Forgetting timeouts, reqwest::Client::default() has none. Always set .timeout().

  • TLS feature flags, reqwest lets you pick rustls or native-tls; pick one and pin it.

  • Blocking work in async tasks, a CPU-bound or blocking call in an async task starves the runtime. Use tokio::task::spawn_blocking.

  • Single-threaded vs multi-threaded runtime, #[tokio::main] defaults to multi-threaded; #[tokio::main(flavor = "current_thread")] for single.