axum rejects bodies over 2 MB by default, but only for extractors that honour DefaultBodyLimit, such as Bytes, String, Json and Form. For a guaranteed cap on every request regardless of how the body is read, add tower-http's RequestBodyLimitLayer as well. Use DefaultBodyLimit to tune per route.
What each one does
The axum documentation says that "for security reasons, Bytes will, by default, not accept bodies larger than 2MB", and that the limit applies to extractors built on Bytes, including String, Json and Form. It is local: it covers only FromRequest implementations that apply it. An extractor that reads the body directly, for example by polling frames, is not covered.
The same page describes how to change it. Apply DefaultBodyLimit::max(n) as a layer, optionally on a single route, or switch it off with DefaultBodyLimit::disable(). When you disable it for untrusted clients, the docs recommend adding your own limit such as one from tower-http.
tower-http's RequestBodyLimitLayer is global. Its documentation says it is applied to all requests, regardless of which extractors are used or how the body is consumed.
How RequestBodyLimitLayer behaves
According to the tower-http docs, when the request declares a Content-Length over the limit, the layer returns an error response before reading any of the body, and the inner service is not called. The status is 413 Payload Too Large. When there is no Content-Length, the body is read until the limit is reached and then fails with a LengthLimitError. In that case your handler or extractor must turn the error into a 413. If the body is never read, no error occurs.
Decision guide
| Situation | Use |
|---|---|
| Standard JSON API with built in extractors | Keep DefaultBodyLimit, tune per route |
| One route accepts large uploads | Raise DefaultBodyLimit on that route only |
| You use third party extractors or read the body yourself | Add RequestBodyLimitLayer |
| You disable DefaultBodyLimit for streaming | Always add RequestBodyLimitLayer |
| Public endpoint, defence in depth | Both, with the layer slightly above the largest valid route |
Example
use axum::{extract::DefaultBodyLimit, routing::post, Router};
use tower_http::limit::RequestBodyLimitLayer;
let app = Router::new()
.route("/api/notes", post(create_note))
.route(
"/api/upload",
post(upload).layer(DefaultBodyLimit::max(25 * 1024 * 1024)),
)
.layer(RequestBodyLimitLayer::new(30 * 1024 * 1024));
Here the upload route allows 25 MB, everything else stays at the 2 MB default, and a hard 30 MB ceiling applies to all requests.
Steps
- List routes and the largest valid body each needs.
- Leave the default for small JSON routes.
- Raise the limit only on routes that need it.
- Add the global layer as a ceiling.
- Test with a body one byte over, with and without a Content-Length header.
- Read the Go equivalent if your team runs both stacks.
Try it yourself
Click through it at your own pace, or open it full screen. Look for how input limit findings are reported against an API.
Quick check
Is 2 MB the right limit for my API?
Probably far too high for most JSON routes. It is a safe default, not a recommendation. Set a smaller limit where the payloads are small.
Does the limit protect against slow bodies?
No. Size and time are different controls. See the post on tower-http timeout layers.
Safeguard's dependency scanning and SBOM generation read Rust lock files, so the crates you ship are inventoried and checked against advisories, and the free scan is at gold.safeguard.sh/scan.
Sources
Last reviewed: 19 October 2026.