Only show guides for your app
Stop abuse and runaway loops with express-rate-limit — return a clean 429 after N requests, choose sensible windows, and move the counter to Redis when one process isn't enough. Verified with a request loop.
An open API is an open invitation — to a buggy client stuck in a loop, a scraper hammering your endpoints, or someone brute-forcing a login. Rate limiting caps how many requests one caller can make in a window, and answers the rest with 429 Too Many Requests instead of falling over. This guide sets it up with express-rate-limit, then shows the honest upgrade to a shared Redis counter.
| At a glance | |
|---|---|
| You need | An Express API on the Node.js application |
| Background | Designing a small REST API |
| Plan | Any plan |
| Time | Twenty minutes |
express-rate-limit counts requests per client (by IP) and blocks the ones over your limit. Install it from the Packages page (search express-rate-limit, install, restart), then wrap the routes you want to protect:
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
const limiter = rateLimit({
windowMs: 60 * 1000, // the window: one minute
limit: 5, // max requests per window, per IP
standardHeaders: 'draft-7', // send RateLimit headers
legacyHeaders: false,
message: { error: 'Too many requests, slow down.' },
});
app.use('/api/', limiter); // protect everything under /api/
app.get('/api/ping', (req, res) => res.json({ ok: true }));
const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));
windowMs and limit are the whole policy: "no more than limit requests per windowMs, per IP." Everything over the line gets an automatic 429 with your message body — you don't write that handler.
Start the server and fire more requests than the limit with a quick loop:
for i in $(seq 1 8); do
curl -s -o /dev/null -w "request $i -> %{http_code}\n" http://YOUR_ADDRESS:PORT/api/ping
done
With limit: 5 you'll see the first five return 200 and the rest return 429:
request 1 -> 200
request 2 -> 200
request 3 -> 200
request 4 -> 200
request 5 -> 200
request 6 -> 429
request 7 -> 429
request 8 -> 429
A 429 response also carries headers a well-behaved client reads to back off:
RateLimit-Policy: 5;w=60
RateLimit: limit=5, remaining=0, reset=60
Retry-After: 60
Retry-After: 60 tells the caller to wait 60 seconds. That's the limiter doing exactly its job.
One global limit rarely fits everything. Give expensive or sensitive routes their own, stricter limiter and leave cheap reads generous:
| Route type | A reasonable start |
|---|---|
General reads (GET) |
60–100 per minute |
Writes (POST/DELETE) |
20–30 per minute |
| Login / password endpoints | 5–10 per 15 minutes — brute-force defence |
| Expensive jobs (uploads, reports) | A handful per minute |
Create a second rateLimit({...}) with tighter numbers and apply it to just those routes:
const loginLimiter = rateLimit({ windowMs: 15 * 60 * 1000, limit: 10 });
app.post('/login', loginLimiter, handleLogin);
🎯 Good to know: Pairing a strict login limiter with hashed passwords is the standard defence against credential-stuffing — see Sessions, JWT & passwords.
Out of the box, express-rate-limit keeps its counters in memory. For a single Falix app server that's genuinely fine — you have one process on one port, so one memory counts everything. But know its two edges:
When either matters, move the counter to Redis so all processes share one tally.
A Redis server (a Redis application on Falix, or a managed one) holds the counts centrally. Install rate-limit-redis and redis from the Packages page, then point the limiter's store at it:
const rateLimit = require('express-rate-limit');
const { RedisStore } = require('rate-limit-redis');
const { createClient } = require('redis');
const client = createClient({ url: process.env.REDIS_URL }); // from .env
await client.connect();
const limiter = rateLimit({
windowMs: 60 * 1000,
limit: 5,
standardHeaders: 'draft-7',
legacyHeaders: false,
store: new RedisStore({ sendCommand: (...args) => client.sendCommand(args) }),
});
app.use('/api/', limiter);
Only the store line changes; the limiter behaves identically — five requests, then 429 — but now the count lives in Redis (keys like rl:<ip>) and survives an app restart, shared across every process pointed at that Redis. Keep the connection URL, with its password, in .env (Environment variables & secrets).
⚠️ Heads up: Redis is now a dependency your app needs to reach. If Redis is unreachable, decide deliberately: fail requests, or fall back to the in-memory store. Don't leave it undefined.
Rate limiting in your app is per-route and understands your logic — the right place for "10 logins per 15 minutes." Your server also has a platform-level Firewall page with its own Rate limiting rules that act on raw connections before they reach Node, useful against blunt floods. They complement each other: the firewall stops the crudest abuse at the edge, your limiter enforces the precise, per-endpoint rules. For a genuine DDoS, the firewall's DDoS protection is the relevant layer, not application code.
The library's own docs at express-rate-limit.mintlify.app cover key generators (limiting by user id instead of IP), skip functions, and every store.
app.set('trust proxy', 1) so the limiter sees the real client IP (Domains and HTTPS).429 never triggers — the limiter isn't in front of the route. app.use('/api/', limiter) must come before the route handlers it should guard.limit, or give heavy routes their own limiter and keep the strict one for login.