Throttling
An attendant at the top of a water slide. They don't turn children away, they make each one wait a bit longer than the last.
Throttling slows requests down after a certain number have gone through, instead of refusing them at a hard cap. The client still gets an answer, later.
A delay instead of a refusal
express-slow-down is middleware, same shape as the limiter: it counts requests in a window and starts delaying past a threshold.
import { slowDown } from 'express-slow-down'
const throttle = slowDown({
windowMs: 10000,
delayAfter: 2,
delayMs: () => 5000,
})
app.get('/api/data', throttle, handler)Three settings: the window, how many get through untouched, and how long to delay everything after that.
Fire five requests at once and the first two return immediately. The other three each wait five seconds, then succeed.
| Request | Arrived | Delay | Responded |
|---|---|---|---|
| 1 | 0s | none | 0s |
| 2 | 0s | none | 0s |
| 3 | 0s | 5s | 5s |
| 4 | 0s | 5s | 5s |
| 5 | 0s | 5s | 5s |
Nothing was refused. Every request succeeded, and three of them took five seconds longer than they wanted to.
That's the appeal and the catch. Nothing breaks, and nothing tells the client to ease off either.
How fast the delay grows
delayMs is a function of hits, the number of requests counted in the window, so the delay can grow with pressure.
delayMs: () => 5000 // fixed: always 5s
delayMs: (hits) => (hits - 2) * 1000 // incremental: 1s, 2s, 3s
delayMs: (hits) => hits * hits * 1000 // exponential: 9s, 16s, 25sWith delayAfter: 2, the three shapes behave very differently by the fifth request:
| Hit | Fixed | Incremental | Exponential |
|---|---|---|---|
| 3 | 5s | 1s | 9s |
| 4 | 5s | 2s | 16s |
| 5 | 5s | 3s | 25s |
Incremental subtracts delayAfter so the first delayed request waits one second rather than three. Exponential squares the hit count, so it climbs steeply enough that sustained pressure becomes painful fast.
The curve you pick matters more than the threshold does, and that only becomes clear with the numbers side by side.
What the window counts
express-slow-down uses a sliding window. Each request looks back windowMs and counts the hits inside it, and if nothing has arrived for longer than that, the count starts fresh.
The subtlety is what counts as being in the window: requests are counted by when they arrived, not when they were processed.
With windowMs: 10000 and delayAfter: 2, five requests arriving at once and five more at 30 seconds:
- The first five all arrive at 0s. Two pass, three are delayed and respond later.
- The second burst arrives at 30s. Looking back ten seconds covers 20s to 30s, and nothing arrived in that stretch, even though delayed responses from the first burst were still going out.
- So the count starts fresh, two pass, and three are delayed again.
Delayed responses leaving the server do not extend the window. Only new arrivals do.
Those waiting children were counted when they arrived. Counting them twice would be charging them for the same visit.
Stacking a throttle and a limiter
The two combine, and the order matters because Express runs middleware left to right:
app.get('/api/data', throttle, limiter, handler)Throttle first means requests get slowed progressively, and only past a hard ceiling does the limiter refuse them. Reverse the order and the limiter cuts clients off before the throttle ever gets to be gentle with them.
A worked brief from the course. The system degrades past three requests in ten seconds, so delay the fourth by 1s, the fifth by 4s, the sixth by 9s. And no more than seven requests in thirty seconds, total.
import { slowDown } from 'express-slow-down'
import { rateLimit } from 'express-rate-limit'
const throttle = slowDown({
windowMs: 10000,
delayAfter: 3,
delayMs: (hits) => (hits - 3) * (hits - 3) * 1000,
})
const limiter = rateLimit({
limit: 7,
windowMs: 30000,
message: 'Too many requests',
standardHeaders: 'draft-6',
legacyHeaders: false,
})The delay formula reads backwards from the required pattern. Hit 4 must give 1, hit 5 must give 4, hit 6 must give 9, which is 1², 2², 3². Subtracting delayAfter turns the hit number into the base, then squaring it produces the curve.
Run seven rapid requests and something unexpected happens. Request 7, delayed sixteen seconds, arrives at the limiter after request 8, which drew a shorter timer. Request 8 becomes the seventh success and request 7 is refused.
Throttled requests are not a queue. Each one holds its own timer and arrives when it arrives.
A leaky bucket would have kept them in line. A throttle hands out timers and lets them run.
Try it
An endpoint is configured with windowMs: 10000, delayAfter: 2, delayMs: (hits) => hits * hits * 1000, and no rate limiter.
- Four requests arrive at once. When does each respond?
- A client sends four requests every 15 seconds, forever. Do the delays get worse over time?
- What happens if a fifth request arrives and its delay exceeds the client's timeout?
- How would you change the configuration so a single client can't slow down everyone else?
Compare your answers
| # | Answer |
|---|---|
| 1 | Requests 1 and 2 respond immediately. Request 3 is hit 3, so 3² = 9 seconds. Request 4 is hit 4, so 4² = 16 seconds |
| 2 | No. The window is 10 seconds and the gap is 15, so each burst looks back at an empty window and starts counting from scratch. The same two-free-then-9s-then-16s pattern repeats forever |
| 3 | The client gives up and your server keeps holding the connection until the delay expires. The work is done and nobody receives it, which is why exponential delays need maxDelayMs |
| 4 | Add a keyGenerator so the throttle counts per client rather than globally. Without one, every request shares a single budget and one heavy client delays everybody |
Question 2 is the one that surprises people. An exponential delay sounds like it punishes sustained abuse, and a client pacing their bursts to fall outside the window never feels it climb at all. The curve only bites inside a single window.
Where this goes next
Five algorithms and two controls, all answering one question: how much is this client allowed to ask for, and what happens when they ask for more.
The reasoning transfers past your own servers. Every external API you call has limits of its own, so understanding these improves your client code too: how it backs off, and how it handles a 429 instead of hammering a service that is already struggling.
That closes the handbook. Five sections, from thinking like an attacker through input safety, identity and abuse prevention, each one built on a working example you can break and then fix.

