Learn / Security / Lesson 07
Size and rate limits
A server with no limits can be knocked over by one request. How caps on body size, request rate and tracked state keep one visitor from taking everyone down.
Last updated: 2026-09-19
What it is
Every input a server accepts has a cost: bytes to read, memory to hold, CPU to parse. Limits put a ceiling on that cost per request and per visitor, so no one client can use it all.
Why it is a rule
- Hash flooding (2011). At the 28C3 conference, Klink and Wälde showed that most web languages stored form fields in hash tables that could be made to collide. A single POST of a few hundred kilobytes, full of carefully chosen field names, could keep a CPU busy for minutes. PHP, Java, Python and Ruby all shipped fixes, including hard caps on the number of form fields per request.
- Credential stuffing. Lists of leaked email-and-password pairs are tried against every login page on the internet. Without a rate limit, a login form is an oracle that checks millions of guesses for free.
How to do it
Cap the body
app.use(express.json({ limit: '512kb' }))
app.use(express.urlencoded({ extended: true, limit: '512kb' }))Pick a limit from what the app really accepts. A file upload route gets its own, larger limit; every other route gets the small one.
Limit the rate, per client
A simple fixed window per IP address is enough for most apps: count requests in the last minute and answer 429 Too Many Requests past the limit, with a Retry-After header.
- Weigh requests by cost. A static image is cheap; a report query is not.
- Login and password-reset routes get a much tighter limit of their own.
- Behind a proxy or CDN, set
trust proxycorrectly, or every visitor looks like the same IP and one busy office locks out everyone.
Bound the limiter itself
A rate limiter that remembers every IP it has ever seen is a memory leak with a public entrance. Cap the number of tracked clients and drop the oldest.
if (clients.size >= MAX_TRACKED) clients.delete(clients.keys().next().value)A Map keeps insertion order, so the first key is the oldest one.
Cap everything else a client can grow
Upload count, open sockets, message size on a websocket, items in a cart, rows in an export. Anything that grows with input needs a ceiling.
How we do it here
Request bodies are capped by default. Every request passes a per-IP limiter that weighs static files lighter than dynamic ones, and the limiter's own memory is capped. Socket messages have a maximum size.
Benefits
- One bad client cannot take the service down for everyone.
- Brute-force and scraping become slow and noisy instead of free.
- Memory stays predictable, which matters on small containers.
Disadvantages
- Limits set too low break real users: a large paste, a busy office behind one IP.
- An in-memory limiter resets on restart and is not shared between servers.
- A distributed attack from thousands of IPs slips under per-IP limits; that needs the CDN or a firewall in front.
Checklist
- Body size capped, with a larger cap only where needed.
- Per-client rate limit, tighter on login.
- The limiter's memory is bounded.
trust proxymatches the real proxy setup.
Sources
- OWASP — Denial of Service Cheat Sheet
- Express — trust proxy
- Alexander Klink and Julian Wälde, Efficient Denial of Service Attacks on Web Application Platforms, 28C3, 2011.