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.
// shop-two-backends not found at build time // shop-two-backends not found at build time // shop-two-backends not found at build time // 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.