Per-route body limits and timeouts
bodyLimitBytes (1 MiB) and requestTimeoutMs (30 s) are set once on the App and apply to every route. Since 1.21.0 a route can override either value for itself. The app value stays the default, so accepting one 10 MiB upload no longer means raising the limit on login, search and webhooks too. The override works in both directions: raise the cap on an upload route, or lower it on a login route to shrink that route's attack surface below the app default.
- 01before routingGlobal onRequest hookbody capped at app bodyLimitBytes
- 02route matchedRoute limit replaces the app limitroute.bodyLimitBytes ?? app.bodyLimitBytes
- 03preBodyAuth, rate limit, CSRFrun before any body byte is read
- 04body readSchema parser or ctx.request413 past the route cap; 408 past route requestTimeoutMs
- 05handlerTyped body within the route's limits
Quick start
Both fields are accepted by app.route({ ... }) and by the app.get / app.post / app.put / app.patch / app.delete shorthands, inside group() and register() as well. A route that omits a field inherits the App value. Routes registered inside a group or plugin resolve against the same App value.
Rules
- Same validation as the App option. A route value must be a finite, non-negative integer.
NaN(for exampleNumber(process.env.UNSET)),Infinity, negatives, non-integers and strings throw atapp.route()time, so a bad environment variable cannot silently disable the limit on one route. An explicitundefinedinherits the App value. bodyLimitBytesapplies to every way the body is read: the schema parser, theContent-Lengthprecheck,ctx.request.text()/json()/arrayBuffer()/formData()/body/clone()in a handler orpreBodyhook, the re-check afterrequestDecompression()inflates a body, and the raw-body fingerprint thatidempotency()computes. Over the cap answers413with the route's own limit in the problem detail.requestTimeoutMscovers the same work as the App option: thepreBody/beforeHandle/afterHandlehooks, the body read and the handler. It answers withrequestTimeoutStatus(default408) and abortsctx.request.signalon expiry.0disables the deadline for that route only.- Auth still runs before the body is read.
bearerAuth(),jwk(),apiKeyAuth(),session()and the otherpreBodyhooks run before a single body byte is pulled, so a raised route behind auth rejects anonymous clients without reading anything. - A global
onRequesthook runs before the route is known. The body is capped at the App limit while that hook runs; once the route is matched the cap is retuned to the route's value. A global hook that reads the body itself is therefore bounded by the App limit, not the route's.
What it does not change
The request body is still buffered in memory before parsing. A 50 MiB route limit means ten concurrent uploads hold about 500 MiB of RAM, so the override is for the few-MB to roughly 25 MiB range. For videos, large PDFs and anything bigger, have your route hand the client a presigned URL and let it upload straight to S3, R2 or GCS; see file uploads.
On Node, serve({ connectionTimeoutMs }) (30 s default) is the socket-level time limit for receiving the whole request and stays server-wide. A per-route requestTimeoutMs extends the framework deadline, not that socket timeout, which is one more reason to keep very large uploads off the server entirely. The multipart.maxFileBytes / maxFiles / maxFields caps also stay App-wide; use fileField({ maxBytes }) for a per-route per-file cap.
Auditing overrides
app.getSecurityPosture().routeOverrides lists every route that overrides either value, with an authenticated flag that is true when an authentication hook is in the route's effective chain:
daloy doctor reads the same list and warns on:
audit.bodyLimit.routeRaisedUnauthenticated: a route raisesbodyLimitBytesabove the App value and nothing authenticates it. Anonymous clients can hold that much memory per request; put auth in front or move the upload to presigned URLs.audit.bodyLimit.routeBlanket: a route cap above 25 MiB.audit.requestTimeout.routeZero: a route that disables its deadline.