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#
tokio-tungstenite, low-level.
axum::extract::ws, in axum.
actix-web has a built-in WebSocket extractor.
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#
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,
reqwestlets 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.