How to Use Tower for Composable Network Services in Rust

Tower enables composable network services in Rust by stacking Service and Layer traits to add features like timeouts and logging.

When middleware becomes a nightmare

You're writing a request handler. It works. Then your team asks for logging. You wrap the handler in a function that prints the request. Then they want timeouts. You wrap the wrapper. Then retries. Then rate limiting. Now you have four layers of boilerplate just to say "do this thing". You're duplicating logic across every endpoint. The code is a nesting doll of async blocks. You can't reuse the logging logic because it's hardcoded into the handler. You can't test the handler without the timeout because they're tangled together.

Tower solves this by letting you stack concerns like LEGO bricks instead of wrapping them like onion skins. You define a core service that does the work. You build layers that add behavior. You compose them into a single stack. The result is clean, reusable, and testable.

Tower turns middleware chaos into a clean stack.

The Service and Layer contract

Tower revolves around two traits: Service and Layer.

A Service is anything that takes a request and returns a future that resolves to a response or an error. It's the core unit of work. Your database query is a service. Your HTTP handler is a service. A mock implementation for testing is a service.

A Layer is a wrapper. It takes a Service and returns a new Service with extra behavior. A logging layer takes a service and returns a service that logs requests. A timeout layer takes a service and returns a service that cancels slow requests.

ServiceBuilder is the tool that stacks layers for you. You chain methods on the builder to add layers, then call .service() or .service_fn() to attach the core service. The builder handles the type complexity so you don't have to.

Think of it like a coffee shop assembly line. The barista makes the coffee. That's the service. You add a wrapper. That's a layer. You add a lid. That's another layer. You add a straw. That's a third layer. The customer gets the final product. Each layer transforms the request or the response without changing the core coffee-making logic.

Layers wrap services. Services do work. That's the whole contract.

Minimal example

This example shows the basics. You define an async function, wrap it with ServiceBuilder, add a concurrency limit and a timeout, then call the service.

use tower::{ServiceBuilder, service_fn, ServiceExt};

/// Handles a simple string request and returns a response.
async fn handle_request(req: String) -> Result<String, std::convert::Infallible> {
    // Return the request wrapped in a success message.
    Ok(format!("Handled: {}", req))
}

#[tokio::main]
async fn main() {
    // Build the service by stacking layers on top of the core handler.
    // ServiceBuilder lets you chain layers declaratively.
    let mut service = ServiceBuilder::new()
        // Limit concurrent requests to 10 to prevent overload.
        .limit_concurrency(10)
        // Add a 5-second timeout to hanging requests.
        .timeout(std::time::Duration::from_secs(5))
        // Wrap the async function into a Service trait object.
        .service_fn(handle_request);

    // Call the service with a request.
    // ready() ensures the service is prepared before calling.
    let response = service
        .ready()
        .await
        .unwrap()
        .call("Hello Tower")
        .await
        .unwrap();

    println!("Response: {}", response);
}

Always call ready() before call(). The compiler won't save you here.

How the stack executes

When you call service.ready().await, the request travels down the stack. Each layer checks if it's ready to accept a request. The concurrency limit layer checks if there are open slots. The timeout layer checks if the timer is available. If any layer says "not ready", the whole service says "not ready". This is backpressure. It prevents you from overwhelming downstream resources.

When ready() returns Ok, you call call(). The request enters the outer layer. The timeout layer starts a timer. It passes the request to the concurrency limit layer. That layer acquires a permit. It passes the request to the core service. The core service runs your handler. The response bubbles back up. The concurrency limit layer releases the permit. The timeout layer stops the timer. You get the response.

The Service trait has two methods: poll_ready and call. poll_ready checks capacity. call sends the request. Most layers delegate poll_ready to their inner service. The ServiceExt trait adds the ready() method that wraps poll_ready in a future so you can use .await.

Convention aside: Service doesn't include ready(). It lives in ServiceExt. You almost always need use tower::ServiceExt to get the ergonomic methods. The community considers this a required import for any Tower code.

Realistic example: Custom logging layer

Built-in layers cover common cases. Sometimes you need custom behavior. This example shows a logging layer that prints requests and responses. It implements Layer and Service manually.

use tower::{Layer, Service, ServiceBuilder, ServiceExt};
use std::task::{Context, Poll};
use std::pin::Pin;
use std::future::Future;

/// A simple logging layer that prints request and response.
struct LoggingLayer;

impl<S> Layer<S> for LoggingLayer {
    type Service = LoggingService<S>;

    fn layer(&self, inner: S) -> Self::Service {
        // Wrap the inner service in the logging wrapper.
        LoggingService { inner }
    }
}

struct LoggingService<S> {
    inner: S,
}

impl<S, Req> Service<Req> for LoggingService<S>
where
    S: Service<Req>,
    S::Future: Unpin,
    S::Error: std::fmt::Debug,
    Req: std::fmt::Debug,
    S::Response: std::fmt::Debug,
{
    type Response = S::Response;
    type Error = S::Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;

    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
        // Delegate readiness check to the inner service.
        self.inner.poll_ready(cx)
    }

    fn call(&mut self, req: Req) -> Self::Future {
        println!("Request: {:?}", req);
        let inner = &mut self.inner;
        // Box the async block to return a trait object future.
        Box::pin(async move {
            let result = inner.call(req).await;
            println!("Response: {:?}", result);
            result
        })
    }
}

#[tokio::main]
async fn main() {
    // Compose the custom layer with built-in layers.
    let mut service = ServiceBuilder::new()
        .layer(LoggingLayer)
        .timeout(std::time::Duration::from_secs(2))
        .service_fn(|req: String| async move {
            Ok::<_, std::convert::Infallible>(format!("Echo: {}", req))
        });

    let res = service.ready().await.unwrap().call("Test").await.unwrap();
    println!("Final: {}", res);
}

Write layers once. Compose them everywhere. That's the leverage.

Pitfalls and compiler errors

Tower code breaks in predictable ways. Here are the common traps.

Forgetting ready()

If you skip ready() and call call() directly, layers like limit_concurrency may fail. The concurrency limit layer returns an error if you call it without checking readiness first. The error isn't a compiler error. It's a runtime error that crashes your handler. Always chain ready().await before call().

Type mismatches

Layers must match the request type. If you chain a layer that expects String but your service takes Vec<u8>, the compiler rejects this with E0277 (trait bound not satisfied). The error message points to the layer that doesn't fit. Check the Request type parameter on each layer.

Clone bounds

ServiceBuilder often needs your service to implement Clone. This happens when you share the service across multiple tasks. If your service holds a non-cloneable resource, the compiler rejects this with E0277 (the trait Clone is not implemented). Wrap the resource in Arc or Rc to fix this.

Unpin futures

If your inner service returns a !Unpin future, you can't use it directly in an async block. You need to use poll! or Box::pin. The compiler rejects this with E0277 (the trait Unpin is not implemented). Add S::Future: Unpin to your bounds or use tower::util::Oneshot to handle the future safely.

Check your types. E0277 is the compiler telling you your layers don't fit the pipe.

Decision matrix

Use Tower when you need to stack cross-cutting concerns like logging, timeouts, retries, or rate limiting across multiple services. Use Tower when you are building a library that exposes a service interface and want users to compose their own middleware. Use Tower when you need a standard Service trait to abstract over different backends, like swapping a real HTTP client for a mock in tests. Reach for plain async functions when you have a single handler with no middleware needs; the overhead of Service isn't worth it for a one-off script. Reach for a full framework like Axum or Actix when you need routing, serialization, and WebSocket support out of the box; Tower sits underneath these frameworks and adds little value if you're just building a standard web app.

Pick the tool that matches your complexity. Don't use a sledgehammer to crack a nut.

Where to go next