Rust's Networking and HTTP Stack
Rust does not have one networking library the way some ecosystems have "the" HTTP client. Instead it has a small set of layers, each built on the one beneath it: raw TCP/UDP sockets at the bottom, hyper implementing the HTTP protocol on top of those sockets, and high-level crates like reqwest (client) and Axum (server) wrapping hyper so that most application code never touches it directly. Understanding this as a stack - not a menu of alternatives - explains why reqwest internally uses hyper, why hyper needs tokio::net underneath it, and why dropping down a layer is occasionally the right call.
This page is the map of that stack. The section's other pages - hyper, reqwest, low-level-sockets, TLS, WebSockets, retries - are all details that live at one specific layer of the picture this page draws.
Summary
- Rust's networking stack is layered - raw sockets, then
hyperas the HTTP engine, then high-level clients and frameworks built onhyper- and each layer trades control for convenience. - Insight: Reaching for the wrong layer either reinvents HTTP badly (working below
hyperwhen you did not need to) or blocks you from customization you actually need (fightingreqwestwhen the problem is really at thehyperor socket layer). - Key Concepts: socket, TCP framing, HTTP engine,
Servicetrait, async client, connection pooling, sync vs. async trade-off. - When to Use: Deciding whether a new integration needs
reqwest, rawhyper, or sockets directly; diagnosing whether a networking bug lives at the transport or protocol layer. - Limitations/Trade-offs: Higher layers buy convenience at the cost of low-level control; lower layers buy control at the cost of reimplementing things
hyperandreqwestalready got right. - Related Topics: async runtimes, TLS, connection pooling, HTTP/2.
Foundations
At the bottom of the stack is the socket: an operating-system handle representing one endpoint of a network connection. A TCP socket gives you a reliable, ordered, connection-oriented byte stream between two hosts; a UDP socket gives you unreliable, unordered, connectionless datagrams. std::net exposes both for blocking code, and tokio::net exposes the same concepts for async code.
The single most important fact about TCP is that it has no concept of "messages." It is a stream of bytes, full stop. If you write "hello" and then "world" to a TCP socket, the receiver might see them as one read of "helloworld," or as two separate reads, or even as "hell" and "oworld" split arbitrarily. Any protocol built on top of TCP - including HTTP - has to define its own rules for where one message ends and the next begins. This is called framing, and it is the reason raw sockets are harder to use correctly than they first appear.
HTTP is one specific, well-defined framing scheme on top of that byte stream: request lines, headers terminated by a blank line, and a body whose length is announced by Content-Length or Transfer-Encoding: chunked. hyper is the Rust crate that implements this framing scheme (HTTP/1 and HTTP/2) so that nothing above it has to parse raw bytes off a socket by hand.
reqwest, the crate most Rust code actually calls to make an outbound HTTP request, is a friendly wrapper around hyper's client machinery: it adds a builder API, JSON (de)serialization via serde, cookie handling, and TLS configuration defaults, so that a typical API call is one chained method call instead of manually assembling an HTTP request.
Mechanics & Interactions
The layering shows up directly in how each crate's types relate to each other. A reqwest::Client internally builds and drives hyper client connections; hyper in turn drives a tokio::net::TcpStream (or a TLS-wrapped stream via rustls) for its actual I/O. Going the other direction, when you write an Axum handler, Axum's router compiles down to a hyper Service - the same abstraction hyper uses to represent "a thing that turns an HTTP request into a response" on both the client and server side.
// The Service trait is what every layer above sockets ultimately implements:
// async fn(Request) -> Result<Response, Error>
//
// Axum routes compile down to a hyper Service.
// reqwest's Client drives hyper's client-side connection machinery.
// Both sit on tokio::net underneath.
async fn hello(_req: hyper::Request<hyper::body::Incoming>)
-> Result<hyper::Response<String>, std::convert::Infallible> {
Ok(hyper::Response::new("ok".into()))
}This is why the question "should I use reqwest or hyper" is really a question about how much of that machinery you want to write yourself. reqwest chooses sensible defaults - connection pooling per host, automatic redirect following, JSON body handling - that cover the overwhelming majority of REST-style API calls. Dropping to hyper directly means implementing (or explicitly opting back into) each of those behaviors, which only pays off for custom protocols, bespoke proxies, or cases where reqwest's defaults actively get in the way.
The sync-versus-async question sits orthogonal to this stack rather than at a specific layer. reqwest::blocking exists as a synchronous facade over the same async hyper machinery, useful in CLI tools and scripts where spinning up a Tokio runtime for a handful of requests is unnecessary ceremony. The trade-off is concurrency: a blocking client making ten requests does them one at a time (or requires manual thread spawning), while an async client can have all ten in flight concurrently on a single thread, because each request yields at its await points instead of parking the thread. For a single request in isolation, async buys nothing - the win only appears once you have many requests (or many connections on a server) competing for the same thread pool.
Advanced Considerations & Applications
Connection reuse is where a surprising number of production networking bugs originate, and it is a direct consequence of the layering above. reqwest::Client and hyper's connection pool both keep TCP (and TLS) connections alive and idle between requests to the same host, because paying for a fresh TCP handshake and TLS negotiation on every call is expensive. This only works, however, if the Client itself is long-lived - constructing a new reqwest::Client per request throws away that pool every time and silently turns every call back into a cold connection. The fix is always the same at every layer of this stack: build the client (or the socket listener) once at startup and share it, not per call.
TLS is inserted as a layer between the socket and hyper, not inside hyper itself: tokio-rustls wraps a TcpStream in an encrypted stream that then gets handed to hyper as if it were a plain socket. This is why reqwest's TLS configuration (via its rustls-tls feature) and a hand-rolled hyper client's TLS setup look structurally similar - both are inserting the same wrapping layer at the same point in the stack.
Protocols above HTTP compound the same layering logic further. A WebSocket connection begins as a normal HTTP request that asks to be "upgraded," after which the same TCP socket is repurposed for a different framing scheme entirely; gRPC (via tonic) runs its own binary framing on top of HTTP/2, which itself runs on top of hyper. In every case, the layer below does not disappear once you move up - it is simply doing its job well enough that the layer above rarely needs to think about it.
| Approach | Strength | Weakness | Best Fit |
|---|---|---|---|
Raw sockets (std::net / tokio::net) | Full control, minimal dependencies, custom framing | You implement message boundaries, retries, and backpressure yourself | Custom binary protocols, game servers, learning HTTP internals |
hyper directly | Full HTTP control without socket-level framing work | Verbose Service/body-trait plumbing; no router or client conveniences | Custom proxies, non-standard HTTP handling, embedding HTTP in unusual systems |
reqwest (client) / Axum (server) | Sensible defaults, connection pooling, JSON, minimal boilerplate | Less control over connection-level behavior unless explicitly configured | The overwhelming majority of REST/JSON client and server code |
Common Misconceptions
- "Async is just faster networking." - Async wins by letting one thread juggle many concurrent connections without blocking. A single request over async is not measurably faster than the same request done synchronously; the benefit only appears under concurrency.
- "reqwest and hyper are competing options." -
reqwestis built onhyper; choosing one does not exclude the other. The real question is which layer of the same stack your code needs to touch. - "TCP delivers messages, not just bytes." - TCP guarantees ordered, reliable delivery of bytes, not message boundaries. Every protocol on top, HTTP included, has to define its own framing.
- "A new Client per request is more isolated and therefore safer." - It throws away connection pooling and reintroduces a full TCP/TLS handshake on every call, which is almost always a performance regression, not a safety improvement.
- "UDP is just 'unreliable TCP.' - UDP is a genuinely different transport with no ordering or delivery guarantees at all; protocols that need those guarantees over UDP (like QUIC) implement them themselves at the application layer.
FAQs
What's the actual relationship between reqwest and hyper?
reqwest is a higher-level client built on top of hyper's connection machinery. It adds a builder API, JSON support, cookie handling, and TLS defaults so most code never calls hyper directly.
Why does TCP need an application-level framing scheme?
TCP only guarantees an ordered, reliable stream of bytes - it has no concept of where one logical message ends and the next begins. HTTP defines that boundary with headers and a declared body length; any custom protocol on raw sockets has to define its own equivalent.
When should I use hyper directly instead of reqwest?
When you are building something reqwest's abstractions actively work against, like a custom proxy, a non-standard protocol layered on HTTP, or fine-grained control over connection behavior that reqwest does not expose.
Does async make a single HTTP request faster?
No. A lone request pays the same network latency either way. Async pays off when many requests or connections are happening concurrently on the same thread.
Why is reusing a Client so important?
A long-lived Client keeps a pool of already-established connections to each host, avoiding a fresh TCP and TLS handshake on every call. Creating a new Client per request throws that pool away every time.
Where does TLS fit into this stack?
Between the raw socket and hyper. tokio-rustls wraps a TcpStream in an encrypted stream, which hyper then treats like any other socket, unaware of the encryption happening beneath it.
Is UDP just a faster, less reliable version of TCP?
Not exactly. It is a different transport model - connectionless, unordered, and without delivery guarantees - suited to workloads where an occasional lost packet is acceptable, like game state updates or discovery broadcasts.
How does a WebSocket relate to the HTTP layer beneath it?
A WebSocket connection begins as a normal HTTP request with an "upgrade" header. Once the server agrees, the same TCP socket is repurposed for the WebSocket framing scheme instead of HTTP.
What is the Service trait and why does it show up everywhere?
It is hyper's abstraction for "something that turns a request into a response," used on both the client and server side. Axum routes compile down to it, and reqwest's client drives the same abstraction internally.
When is reqwest::blocking a reasonable choice?
In CLI tools or scripts making a handful of sequential requests, where spinning up a full Tokio runtime for async concurrency is unnecessary overhead relative to the work being done.
Why do custom protocols still get built on raw sockets today?
When HTTP's overhead (headers, text-based framing) or its request/response model does not fit - low-latency game servers and some internal binary protocols are common cases - going below hyper trades convenience for control that HTTP's abstractions do not offer.
Does gRPC skip this networking stack?
No. tonic runs gRPC's binary framing on top of HTTP/2, which itself runs on hyper, which runs on tokio::net sockets - it is another layer added to the same stack, not a replacement for it.
Related
- Low-Level Sockets - the transport layer beneath HTTP
- hyper - the HTTP engine at the center of this stack
- HTTP Clients (reqwest) - the high-level client built on hyper
- TLS with rustls - the encryption layer inserted below hyper
- WebSockets - an upgraded connection built on this same stack
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.