---
title: Size and rate limits
summary: 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.
topic: security
order: 7
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

```js
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 proxy` correctly, 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.

```js
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 proxy` matches the real proxy setup.

## Sources

- [OWASP — Denial of Service Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html)
- [Express — trust proxy](https://expressjs.com/en/guide/behind-proxies.html)
- Alexander Klink and Julian Wälde, *Efficient Denial of Service Attacks on Web Application Platforms*, 28C3, 2011.
