One Request, Fully Traced
Consider this handler:
async fn create_user(
State(db): State<Arc<Database>>,
Path(team): Path<String>,
Json(input): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), ApiError> { /* ... */ }
1. A connection gets its own task
serve awaits Listener::accept, constructs a service for the connection,
adapts Tower’s service to Hyper, and asks its Executor to run the connection
future. The default executor calls tokio::spawn.
Source: serve/mod.rs:563
2. Hyper calls the router as a service
Hyper owns HTTP parsing and connection behavior. When a request is available,
the adapter calls Router<()> through Tower’s Service<Request<B>>. Axum
normalizes the body to its own Body and enters call_with_state.
Source: routing/mod.rs:599
3. Routing refines the destination twice
PathRouter matches the URI path, stores captured parameters in request
extensions, then forwards to an endpoint. MethodRouter selects the GET, POST,
or other method service. A failed path or method becomes a fallback response
rather than an escaped routing error.
Sources: path_router.rs:325,
method_routing.rs:1200
4. The handler adapter splits the request
The macro-generated Handler implementation separates Parts from Body.
It runs State and Path through FromRequestParts, in argument order. Each
may inspect or mutate metadata, but cannot consume the body.
The final Json argument implements FromRequest and receives the reconstructed
whole request. This type distinction makes “the body can be consumed once” a
compile-time rule.
Source: handler/mod.rs:221
5. The handler future is awaited
Only after all extraction succeeds does Axum call create_user. Awaiting the
database does not block the worker thread: the request future returns Pending,
and Tokio may use that thread to poll other ready tasks.
No new task is created merely because the handler is async. It is one nested
future inside Hyper’s connection machinery.
6. One concrete response type leaves the boundary
The handler may return many Rust types, but IntoResponse normalizes them. Both
Ok((StatusCode, Json<User>)) and Err(ApiError) implement IntoResponse, so
Result<T, E> can become the single HTTP Response expected by the service.
Source: into_response.rs:141
accept socket → spawn connection → Hyper parses request → Router::call
→ match path → match method → extract parts → consume body
→ await handler → IntoResponse → Hyper writes response body
This is sequential dependency inside one request. Concurrency appears because many connection and request futures can be suspended and polled independently.