Skip to content

← all backend comparisons

middleware

Rate-limit middleware: chi.Use vs tower::Layer

100 requests per second per IP, fixed window, in memory. The Go side is one function (func(http.Handler) http.Handler) wired in via chi's r.Use. The Rust side is a Layer + a Service impl — two trait impls and a poll_ready dance — for the same outcome.

Go (chi · sqlc · pgx)
One function. That is the entire middleware contract.
go/internal/httpserver/rate.go
// shop-two-backends not found at build time
Wired with one r.Use(...) line
go/internal/httpserver/server.go
// shop-two-backends not found at build time
Rust (axum · sqlx · tokio)
Layer<S> + Service<Request<Body>> with poll_ready and a boxed future
rust/src/middleware/rate_limit.rs
// shop-two-backends not found at build time
Wired with one .layer(...) line
rust/src/main.rs
// shop-two-backends not found at build time

What to take away

This is the page where Go's simplicity wins on its own merits.

Go's middleware is a function from http.Handler to http.Handler. To compose, you wrap. To skip, you call next.ServeHTTP. To short-circuit, you don't. Forty lines for the limiter including the bucket struct and IP extraction; chi calls it via one r.Use(...). There is nothing more to learn.

Rust's tower stack is more powerful and more ceremonial. Layer<S> is the factory: "given an inner service, give me back the wrapped one." Service<Request> is the runtime: poll_ready for backpressure, call for the request itself. Async-fn-in-trait isn't quite there for Service yet, so the future is boxed (Pin<Box<dyn Future>>). Cloning the inner before mem::replace is required because Tower's contract permits call before the next poll_ready — get this wrong and you'll deadlock under load.

The Rust version's payoff is that the same Layer plugs into any Tower-aware service: axum, hyper, tonic (gRPC), every embedded HTTP server. The Go middleware is bound to the net/http shape — chi, gorilla, and stdlib all compose, but anything outside that family (e.g. fasthttp, gRPC) requires a different middleware function.

Verdict: for "limit requests per IP", the Go version is shorter, simpler, and equally correct. The Rust ceremony earns its keep when you actually need to share middleware across protocols — most apps don't. This page is one of the cleanest examples in the project of where Rust's type system asks you to pay for power you may never use.