Let me be honest with you.
Every time I start a new Node.js project, I copy-paste this from my last one:
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100,
});
That 15 * 60 * 1000 has always bothered me. It's not a big deal. But it's also... not great? I have to do math in my head every time I read it. Is that 15 minutes? Let me check. 15 * 60 = 900. 900 * 1000 = 900000. Yes, 15 minutes.
Why am I doing mental arithmetic in 2026?
That small frustration was the start of @chabdulwahab/flowcap.
The actual problem
express-rate-limit is great — 8.6 million weekly downloads great. But it has two issues that quietly annoy me:
1. It's Express-only. If you switch to Fastify or Koa, you need a different package. fastify-rate-limit, koa-ratelimit — each with its own API, its own quirks, its own docs to read.
2. The window is in milliseconds. Every single time. You always end up writing 15 * 60 * 1000 and adding a comment to explain what it means. The comment is the tell — the code isn't readable without it.
rate-limiter-flexible solves the framework problem, but introduces a different one: complexity. It's powerful, but you're reading docs for 20 minutes before you write a single line.
I wanted something in between. Dead simple. Works anywhere. Zero deps. Human-readable.
That's flowcap.
What flowcap does differently
1. Human-readable time windows
// Before — what does this even mean at a glance?
windowMs: 15 * 60 * 1000
// After — obvious
window: '15m'
Supported formats: '500ms', '30s', '15m', '2h', '1d'. Pass a number and it treats it as milliseconds — backwards compatible if you prefer.
2. Works on any framework
The middleware signature is just (req, res, next). That's the universal contract every Node.js framework follows. So flowcap works on all of them:
// Express
app.use(flowcap({ limit: 100, window: '1m' }));
// Fastify
fastify.addHook('onRequest', flowcap({ limit: 100, window: '1m' }));
// Koa
app.use((ctx, next) => flowcap({ limit: 100, window: '1m' })(ctx.req, ctx.res, next));
// Vanilla http
http.createServer((req, res) => {
flowcap({ limit: 100, window: '1m' })(req, res, () => {
// your handler
});
});
3. Built-in presets for common use cases
The most common rate limiting scenarios are always the same: protect your login endpoint, set a standard API limit, lock down sensitive admin routes. So I built presets:
// Brute-force protection on login (5 requests per 15 minutes)
app.post('/login', flowcap.login(), handler);
// Standard API (100 requests per minute)
app.use('/api', flowcap.api());
// Sensitive endpoint (20 per minute)
app.get('/admin', flowcap.strict(), handler);
// High-traffic public route (500 per minute)
app.get('/feed', flowcap.loose(), handler);
Every preset is just a default config — you can override anything:
// Login preset, but stricter
app.post('/login', flowcap.login({ limit: 3 }), handler);
4. IETF standard headers, automatically
Every response gets the RateLimit header from IETF draft-8:
RateLimit: limit=100, remaining=87, reset=1716300000
Plus legacy headers for clients that expect them:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87
X-RateLimit-Reset: 1716300000
On 429 responses, Retry-After is set automatically so clients know exactly when to retry.
How it works under the hood
flowcap uses a sliding window log algorithm. Here's the core idea:
For each unique key (IP address by default), we store a log of request timestamps in memory:
key: "192.168.1.1"
timestamps: [1716300000, 1716300010, 1716300025, 1716300041]
On every incoming request:
-
Prune — remove timestamps older than
now - windowMs - Count — how many timestamps remain?
-
Allow or block — if
count >= limit, return 429. Otherwise, add current timestamp and callnext()
This is more accurate than the fixed window algorithm (which express-rate-limit uses by default) because it doesn't have the "boundary burst" problem — where a user can make 2x limit requests by hammering the window boundary.
The store is a simple Map:
class Store {
constructor() {
this.records = new Map(); // key → number[]
}
prune(key, windowMs) {
const cutoff = Date.now() - windowMs;
const timestamps = this.records.get(key) || [];
const fresh = timestamps.filter(t => t > cutoff);
fresh.length === 0
? this.records.delete(key)
: this.records.set(key, fresh);
return fresh;
}
count(key, windowMs) {
return this.prune(key, windowMs).length;
}
add(key, windowMs) {
const fresh = this.prune(key, windowMs);
fresh.push(Date.now());
this.records.set(key, fresh);
}
}
Zero dependencies. Pure Node.js. The Map handles memory, pruning keeps it clean on every request — no separate cleanup timer needed.
The full API
flowcap({
// Core
limit: 100, // max requests per window
window: '1m', // '30s', '15m', '2h', '1d', or ms number
// Key extraction (default: IP address)
keyBy: (req) => req.ip,
// Skip certain requests entirely
skip: (req) => req.path === '/health',
// Custom 429 response
onLimit: (req, res, next) => {
res.status(429).json({ message: 'Slow down.' });
},
// Header control
legacyHeaders: true, // set false to disable X-RateLimit-* headers
// Advanced: bring your own store
store: myCustomStore,
})
The keyBy option is where things get interesting. Instead of IP-based limiting, you can limit per user:
// Rate limit by authenticated user ID
app.use(flowcap({
limit: 1000,
window: '1h',
keyBy: (req) => req.user?.id || req.ip,
}));
// Rate limit by API key
app.use(flowcap({
limit: 500,
window: '1m',
keyBy: (req) => req.headers['x-api-key'] || req.ip,
}));
Quick comparison
flowcap |
express-rate-limit |
rate-limiter-flexible |
|
|---|---|---|---|
| Framework agnostic | ✅ | ❌ Express only | ✅ |
| Human-readable window | ✅ '15m'
|
❌ 900000
|
❌ ms only |
| Zero dependencies | ✅ | ✅ | ✅ |
| Built-in presets | ✅ | ❌ | ❌ |
| TypeScript types | ✅ | ✅ | ✅ |
| Redis support | ❌ v2 | ✅ | ✅ |
| Algorithm | Sliding window | Fixed window | Both |
| Setup time | ~30 seconds | ~2 minutes | ~10 minutes |
flowcap is intentionally not trying to replace rate-limiter-flexible for complex distributed use cases. If you need Redis, Mongo, or cluster support — use that. It's excellent. flowcap is for the 80% case: single-process apps that just need rate limiting to work without thinking about it.
Install and try it
npm install @chabdulwahab/flowcap
const flowcap = require('@chabdulwahab/flowcap');
// That's it. Protect your whole API in one line.
app.use(flowcap({ limit: 100, window: '15m' }));
The package is 3.7 KB on disk. No setup. No config files. No external services.
What's next
v1 is deliberately minimal — in-memory, single-process. The roadmap for v2:
- Redis adapter for multi-instance deployments
- Fastify plugin wrapper for native
fastify.register()support -
flowcap.byRoute()helper for different limits per route pattern
If you try it and something doesn't work the way you expect, open an issue. I built this because I wanted it to exist — feedback from real usage is what makes it actually good.
@chabdulwahab/flowcap on npm → npmjs.com/package/@chabdulwahab/flowcap
This is my third indie npm package — the others are @chabdulwahab/env-ok (zero-dep env validator) and @chabdulwahab/api-spy (terminal metrics dashboard). I build small, focused tools that solve one thing well. If that's your kind of software, follow along.
Top comments (0)