nextlimiter 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +438 -438
- package/package.json +2 -2
- package/src/analytics/tracker.js +107 -107
- package/src/core/config.js +127 -127
- package/src/core/limiter.js +229 -229
- package/src/index.js +108 -106
- package/src/smart/detector.js +117 -117
- package/src/store/memoryStore.js +122 -122
- package/src/store/redisStore.js +150 -0
- package/src/strategies/fixedWindow.js +40 -40
- package/src/strategies/slidingWindow.js +78 -78
- package/src/strategies/tokenBucket.js +85 -85
- package/src/utils/keyGenerator.js +95 -95
- package/src/utils/logger.js +76 -76
- package/types/index.d.ts +277 -246
package/README.md
CHANGED
|
@@ -1,438 +1,438 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
**Production-ready rate limiting for Node.js — simple, smart, and built for real SaaS apps.**
|
|
4
|
-
|
|
5
|
-
[](LICENSE)
|
|
7
|
-
[]()
|
|
8
|
-
|
|
9
|
-
---
|
|
10
|
-
|
|
11
|
-
## Why
|
|
12
|
-
|
|
13
|
-
Most rate limiting libraries make you choose between simple-but-limited and powerful-but-complex.
|
|
14
|
-
|
|
15
|
-
| Feature | express-rate-limit | rate-limiter-flexible | **
|
|
16
|
-
|---|---|---|---|
|
|
17
|
-
| Zero-config usage | ✓ | ✗ | ✓ |
|
|
18
|
-
| SaaS plan tiers | ✗ | ✗ | **✓** |
|
|
19
|
-
| Smart / behavior-based limiting | ✗ | ✗ | **✓** |
|
|
20
|
-
| Built-in analytics | ✗ | ✗ | **✓** |
|
|
21
|
-
| Programmatic `check()` API | ✗ | ✓ | ✓ |
|
|
22
|
-
| Named presets | ✗ | ✗ | **✓** |
|
|
23
|
-
| TypeScript types included | ✓ | ✓ | ✓ |
|
|
24
|
-
| Zero dependencies | ✓ | ✗ | ✓ |
|
|
25
|
-
|
|
26
|
-
---
|
|
27
|
-
|
|
28
|
-
## Installation
|
|
29
|
-
|
|
30
|
-
```bash
|
|
31
|
-
npm install
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
No Redis required. Works out of the box with in-memory storage.
|
|
35
|
-
|
|
36
|
-
---
|
|
37
|
-
|
|
38
|
-
## Quick Start
|
|
39
|
-
|
|
40
|
-
### Zero-config (one line)
|
|
41
|
-
|
|
42
|
-
```js
|
|
43
|
-
const { autoLimit } = require('
|
|
44
|
-
app.use(autoLimit());
|
|
45
|
-
// → 100 requests/min per IP, sliding window, no setup needed
|
|
46
|
-
```
|
|
47
|
-
|
|
48
|
-
### Custom configuration
|
|
49
|
-
|
|
50
|
-
```js
|
|
51
|
-
const { createLimiter } = require('
|
|
52
|
-
|
|
53
|
-
const limiter = createLimiter({
|
|
54
|
-
windowMs: 60_000, // 1 minute
|
|
55
|
-
max: 100, // max 100 requests per window
|
|
56
|
-
strategy: 'sliding-window',
|
|
57
|
-
logging: true,
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
app.use('/api', limiter.middleware());
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
---
|
|
64
|
-
|
|
65
|
-
## Core Concepts
|
|
66
|
-
|
|
67
|
-
### Strategies
|
|
68
|
-
|
|
69
|
-
#### `sliding-window` (default)
|
|
70
|
-
The most accurate algorithm. Uses a weighted two-window approximation — same approach as Cloudflare and Nginx's `limit_req_zone`. No boundary-burst problem. O(1) memory per key.
|
|
71
|
-
|
|
72
|
-
```js
|
|
73
|
-
createLimiter({ strategy: 'sliding-window', windowMs: 60_000, max: 100 })
|
|
74
|
-
```
|
|
75
|
-
|
|
76
|
-
#### `token-bucket`
|
|
77
|
-
Tokens refill continuously. Allows controlled bursts up to `max` tokens while enforcing a sustained rate. Used by Stripe for their API. Best for APIs where occasional spikes are expected.
|
|
78
|
-
|
|
79
|
-
```js
|
|
80
|
-
createLimiter({ strategy: 'token-bucket', windowMs: 60_000, max: 100 })
|
|
81
|
-
```
|
|
82
|
-
|
|
83
|
-
#### `fixed-window`
|
|
84
|
-
Simplest approach. Counts requests in fixed time intervals. Lowest memory usage. Note: susceptible to boundary-burst attacks (a client can use 2× the limit by straddling a window boundary).
|
|
85
|
-
|
|
86
|
-
```js
|
|
87
|
-
createLimiter({ strategy: 'fixed-window', windowMs: 60_000, max: 100 })
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
---
|
|
91
|
-
|
|
92
|
-
## Features
|
|
93
|
-
|
|
94
|
-
### SaaS Plan-Based Limiting
|
|
95
|
-
|
|
96
|
-
Apply different rate limits based on subscription tier without writing conditional logic:
|
|
97
|
-
|
|
98
|
-
```js
|
|
99
|
-
const { createPlanLimiter } = require('
|
|
100
|
-
|
|
101
|
-
// Built-in plans: free (60/min), pro (600/min), enterprise (6000/min)
|
|
102
|
-
const limiter = createPlanLimiter('pro', {
|
|
103
|
-
keyBy: 'api-key',
|
|
104
|
-
logging: true,
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
app.use('/api', limiter.middleware());
|
|
108
|
-
```
|
|
109
|
-
|
|
110
|
-
**Custom plan definitions:**
|
|
111
|
-
|
|
112
|
-
```js
|
|
113
|
-
const limiter = createLimiter({
|
|
114
|
-
plans: {
|
|
115
|
-
startup: { windowMs: 60_000, max: 150, burstMax: 20 },
|
|
116
|
-
growth: { windowMs: 60_000, max: 500, burstMax: 80 },
|
|
117
|
-
enterprise: { windowMs: 60_000, max: 5000, burstMax: 500 },
|
|
118
|
-
},
|
|
119
|
-
plan: 'startup', // swap this based on req.user.plan at runtime
|
|
120
|
-
});
|
|
121
|
-
```
|
|
122
|
-
|
|
123
|
-
**Dynamic plan selection per request:**
|
|
124
|
-
|
|
125
|
-
```js
|
|
126
|
-
// Create a limiter per plan, pick the right one in your route handler
|
|
127
|
-
const planLimiters = {
|
|
128
|
-
free: createPlanLimiter('free', { keyBy: 'api-key' }),
|
|
129
|
-
pro: createPlanLimiter('pro', { keyBy: 'api-key' }),
|
|
130
|
-
enterprise: createPlanLimiter('enterprise', { keyBy: 'api-key' }),
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
app.use('/api', (req, res, next) => {
|
|
134
|
-
const plan = req.user?.plan || 'free';
|
|
135
|
-
return planLimiters[plan].middleware()(req, res, next);
|
|
136
|
-
});
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
### Smart Rate Limiting
|
|
142
|
-
|
|
143
|
-
Detects burst traffic and dynamically reduces limits for suspicious clients — without blocking them entirely.
|
|
144
|
-
|
|
145
|
-
```js
|
|
146
|
-
const limiter = createLimiter({
|
|
147
|
-
windowMs: 60_000,
|
|
148
|
-
max: 100,
|
|
149
|
-
smart: true,
|
|
150
|
-
smartThreshold: 2.0, // flag if rate exceeds 2× normal
|
|
151
|
-
smartCooldownMs: 60_000, // penalty lasts 60 seconds
|
|
152
|
-
smartPenaltyFactor: 0.5, // reduce limit to 50% during penalty
|
|
153
|
-
});
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
**How it works:**
|
|
157
|
-
1. Tracks the request rate for each key in a short observation window (10% of `windowMs`)
|
|
158
|
-
2. If rate exceeds `normalRate × smartThreshold`, the key is flagged
|
|
159
|
-
3. Flagged keys get `floor(max × smartPenaltyFactor)` as their effective limit
|
|
160
|
-
4. Penalty expires after `smartCooldownMs`
|
|
161
|
-
5. Legitimate users are completely unaffected
|
|
162
|
-
|
|
163
|
-
---
|
|
164
|
-
|
|
165
|
-
### Named Presets
|
|
166
|
-
|
|
167
|
-
Four built-in presets for the most common scenarios:
|
|
168
|
-
|
|
169
|
-
```js
|
|
170
|
-
const { createPresetLimiter } = require('
|
|
171
|
-
|
|
172
|
-
// Strict — 30 req/min, sliding window, smart limiting on
|
|
173
|
-
app.use('/admin', createPresetLimiter('strict').middleware());
|
|
174
|
-
|
|
175
|
-
// Relaxed — 300 req/min, token bucket
|
|
176
|
-
app.use('/public', createPresetLimiter('relaxed').middleware());
|
|
177
|
-
|
|
178
|
-
// API — 100 req/min, api-key based, smart on
|
|
179
|
-
app.use('/api', createPresetLimiter('api').middleware());
|
|
180
|
-
|
|
181
|
-
// Auth — 10 attempts per 15 minutes (brute-force protection)
|
|
182
|
-
app.post('/login', createPresetLimiter('auth').middleware());
|
|
183
|
-
```
|
|
184
|
-
|
|
185
|
-
---
|
|
186
|
-
|
|
187
|
-
### Built-in Analytics
|
|
188
|
-
|
|
189
|
-
Every limiter instance tracks metrics automatically:
|
|
190
|
-
|
|
191
|
-
```js
|
|
192
|
-
const stats = limiter.getStats();
|
|
193
|
-
|
|
194
|
-
console.log(stats);
|
|
195
|
-
// {
|
|
196
|
-
// totalRequests: 15420,
|
|
197
|
-
// blockedRequests: 234,
|
|
198
|
-
// allowedRequests: 15186,
|
|
199
|
-
// blockRate: 0.0152,
|
|
200
|
-
// topKeys: [
|
|
201
|
-
// { key: '
|
|
202
|
-
// { key: '
|
|
203
|
-
// ],
|
|
204
|
-
// topBlocked: [
|
|
205
|
-
// { key: '
|
|
206
|
-
// ],
|
|
207
|
-
// trackedSince: '2024-01-15T10:00:00.000Z',
|
|
208
|
-
// uptimeMs: 3600000,
|
|
209
|
-
// config: {
|
|
210
|
-
// strategy: 'sliding-window',
|
|
211
|
-
// windowMs: 60000,
|
|
212
|
-
// max: 100,
|
|
213
|
-
// plan: 'pro',
|
|
214
|
-
// smart: true,
|
|
215
|
-
// }
|
|
216
|
-
// }
|
|
217
|
-
|
|
218
|
-
// Expose as an endpoint (protect with auth in production)
|
|
219
|
-
app.get('/admin/stats', (req, res) => res.json(limiter.getStats()));
|
|
220
|
-
|
|
221
|
-
// Reset counters
|
|
222
|
-
limiter.resetStats();
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
---
|
|
226
|
-
|
|
227
|
-
### Key-Based Limiting
|
|
228
|
-
|
|
229
|
-
**By IP (default):**
|
|
230
|
-
```js
|
|
231
|
-
createLimiter({ keyBy: 'ip' })
|
|
232
|
-
// Uses X-Forwarded-For → X-Real-IP → req.ip (proxy-aware)
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
**By authenticated user ID:**
|
|
236
|
-
```js
|
|
237
|
-
createLimiter({ keyBy: 'user-id' })
|
|
238
|
-
// Reads req.user.id → req.user._id → req.userId
|
|
239
|
-
// Falls back to IP for unauthenticated requests
|
|
240
|
-
```
|
|
241
|
-
|
|
242
|
-
**By API key:**
|
|
243
|
-
```js
|
|
244
|
-
createLimiter({ keyBy: 'api-key' })
|
|
245
|
-
// Reads Authorization: Bearer <token> → X-API-Key header → ?apiKey query param
|
|
246
|
-
```
|
|
247
|
-
|
|
248
|
-
**Custom key function:**
|
|
249
|
-
```js
|
|
250
|
-
createLimiter({
|
|
251
|
-
keyGenerator: (req) => `tenant:${req.headers['x-tenant-id']}`,
|
|
252
|
-
})
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
---
|
|
256
|
-
|
|
257
|
-
### Programmatic API
|
|
258
|
-
|
|
259
|
-
Use `limiter.check()` for rate limiting outside HTTP middleware — WebSockets, background jobs, cron tasks:
|
|
260
|
-
|
|
261
|
-
```js
|
|
262
|
-
const limiter = createLimiter({ windowMs: 60_000, max: 10 });
|
|
263
|
-
|
|
264
|
-
// WebSocket message handler
|
|
265
|
-
async function onMessage(userId, message) {
|
|
266
|
-
const result = await limiter.check(`ws:${userId}`);
|
|
267
|
-
|
|
268
|
-
if (!result.allowed) {
|
|
269
|
-
socket.emit('error', {
|
|
270
|
-
message: 'Rate limit exceeded',
|
|
271
|
-
retryAfter: result.retryAfter,
|
|
272
|
-
});
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
processMessage(message);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
// Background job
|
|
280
|
-
async function runExport(userId) {
|
|
281
|
-
const result = await limiter.check(`export:${userId}`);
|
|
282
|
-
if (!result.allowed) throw new Error(`Try again in ${result.retryAfter}s`);
|
|
283
|
-
// ... run export
|
|
284
|
-
}
|
|
285
|
-
```
|
|
286
|
-
|
|
287
|
-
---
|
|
288
|
-
|
|
289
|
-
### Developer-Friendly Logging
|
|
290
|
-
|
|
291
|
-
```js
|
|
292
|
-
createLimiter({ logging: true, logPrefix: '[API]' })
|
|
293
|
-
```
|
|
294
|
-
|
|
295
|
-
Output:
|
|
296
|
-
```
|
|
297
|
-
2024-01-15T10:23:41.000Z [API] BLOCKED ip:1.2.3.4 (101/100) via sliding-window
|
|
298
|
-
2024-01-15T10:23:42.000Z [API] BLOCKED ip:1.2.3.4 (45/22) via sliding-window [smart]
|
|
299
|
-
```
|
|
300
|
-
|
|
301
|
-
Colors are automatically disabled in non-TTY environments (CI, Docker logs).
|
|
302
|
-
|
|
303
|
-
---
|
|
304
|
-
|
|
305
|
-
### Skip and Custom Handlers
|
|
306
|
-
|
|
307
|
-
```js
|
|
308
|
-
createLimiter({
|
|
309
|
-
// Skip rate limiting for specific requests
|
|
310
|
-
skip: (req) =>
|
|
311
|
-
req.path === '/health' ||
|
|
312
|
-
req.headers['x-internal-service'] === 'true' ||
|
|
313
|
-
req.ip === '127.0.0.1',
|
|
314
|
-
|
|
315
|
-
// Full control over the blocked response
|
|
316
|
-
onLimitReached: (req, res, result) => {
|
|
317
|
-
res.status(429).json({
|
|
318
|
-
error: 'Rate limit exceeded',
|
|
319
|
-
retryAfter: result.retryAfter,
|
|
320
|
-
upgrade: 'Upgrade to Pro for 10× the rate limit',
|
|
321
|
-
docsUrl: 'https://yourapp.com/docs/rate-limits',
|
|
322
|
-
});
|
|
323
|
-
},
|
|
324
|
-
})
|
|
325
|
-
```
|
|
326
|
-
|
|
327
|
-
---
|
|
328
|
-
|
|
329
|
-
## Full Configuration Reference
|
|
330
|
-
|
|
331
|
-
| Option | Type | Default | Description |
|
|
332
|
-
|---|---|---|---|
|
|
333
|
-
| `windowMs` | `number` | `60000` | Time window in milliseconds |
|
|
334
|
-
| `max` | `number` | `100` | Max requests per window |
|
|
335
|
-
| `strategy` | `string` | `'sliding-window'` | `'fixed-window'` \| `'sliding-window'` \| `'token-bucket'` |
|
|
336
|
-
| `keyBy` | `string\|fn` | `'ip'` | `'ip'` \| `'user-id'` \| `'api-key'` \| `(req) => string` |
|
|
337
|
-
| `keyPrefix` | `string` | `'
|
|
338
|
-
| `plan` | `string` | `null` | `'free'` \| `'pro'` \| `'enterprise'` |
|
|
339
|
-
| `plans` | `object` | built-in | Custom plan definitions |
|
|
340
|
-
| `preset` | `string` | `null` | `'strict'` \| `'relaxed'` \| `'api'` \| `'auth'` |
|
|
341
|
-
| `smart` | `boolean` | `false` | Enable smart burst detection |
|
|
342
|
-
| `smartThreshold` | `number` | `2.0` | Rate multiplier that triggers penalty |
|
|
343
|
-
| `smartCooldownMs` | `number` | `60000` | How long smart penalty lasts |
|
|
344
|
-
| `smartPenaltyFactor` | `number` | `0.5` | Limit multiplier during penalty (0–1) |
|
|
345
|
-
| `logging` | `boolean` | `false` | Enable console logging |
|
|
346
|
-
| `logPrefix` | `string` | `'[
|
|
347
|
-
| `headers` | `boolean` | `true` | Send `X-RateLimit-*` headers |
|
|
348
|
-
| `statusCode` | `number` | `429` | HTTP status for blocked requests |
|
|
349
|
-
| `message` | `string` | `'Too many requests...'` | Default 429 message |
|
|
350
|
-
| `store` | `Store` | `MemoryStore` | Custom storage backend |
|
|
351
|
-
| `skip` | `fn` | `null` | `(req) => boolean` — skip rate limiting |
|
|
352
|
-
| `onLimitReached` | `fn` | `null` | `(req, res, result) => void` |
|
|
353
|
-
| `keyGenerator` | `fn` | `null` | `(req) => string` — override key generation |
|
|
354
|
-
|
|
355
|
-
---
|
|
356
|
-
|
|
357
|
-
## Response Headers
|
|
358
|
-
|
|
359
|
-
Every response includes these headers:
|
|
360
|
-
|
|
361
|
-
```
|
|
362
|
-
X-RateLimit-Limit: 100
|
|
363
|
-
X-RateLimit-Remaining: 43
|
|
364
|
-
X-RateLimit-Reset: 1705315200
|
|
365
|
-
X-RateLimit-Strategy: sliding-window
|
|
366
|
-
Retry-After: 47 ← only on 429 responses
|
|
367
|
-
```
|
|
368
|
-
|
|
369
|
-
---
|
|
370
|
-
|
|
371
|
-
## Custom Store (Redis example)
|
|
372
|
-
|
|
373
|
-
Implement the `Store` interface to use any backend:
|
|
374
|
-
|
|
375
|
-
```js
|
|
376
|
-
const Redis = require('ioredis');
|
|
377
|
-
|
|
378
|
-
class RedisStore {
|
|
379
|
-
constructor(client) {
|
|
380
|
-
this.client = client;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
async get(key) {
|
|
384
|
-
const val = await this.client.get(key);
|
|
385
|
-
return val ? JSON.parse(val) : undefined;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
async set(key, value, ttlMs) {
|
|
389
|
-
await this.client.set(key, JSON.stringify(value), 'PX', ttlMs);
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
async increment(key, ttlMs) {
|
|
393
|
-
const count = await this.client.incr(key);
|
|
394
|
-
if (count === 1) await this.client.pexpire(key, ttlMs);
|
|
395
|
-
return count;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
async delete(key) {
|
|
399
|
-
await this.client.del(key);
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
keys() { return []; } // optional
|
|
403
|
-
clear() {} // optional
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// Use it:
|
|
407
|
-
const limiter = createLimiter({
|
|
408
|
-
store: new RedisStore(new Redis()),
|
|
409
|
-
windowMs: 60_000,
|
|
410
|
-
max: 100,
|
|
411
|
-
});
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
---
|
|
415
|
-
|
|
416
|
-
## TypeScript
|
|
417
|
-
|
|
418
|
-
Full TypeScript support included — no `@types/
|
|
419
|
-
|
|
420
|
-
```ts
|
|
421
|
-
import { createLimiter, LimiterOptions, RateLimitResult, Store } from '
|
|
422
|
-
|
|
423
|
-
const options: LimiterOptions = {
|
|
424
|
-
windowMs: 60_000,
|
|
425
|
-
max: 100,
|
|
426
|
-
strategy: 'token-bucket',
|
|
427
|
-
smart: true,
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
const limiter = createLimiter(options);
|
|
431
|
-
const result: RateLimitResult = await limiter.check('user:42');
|
|
432
|
-
```
|
|
433
|
-
|
|
434
|
-
---
|
|
435
|
-
|
|
436
|
-
## License
|
|
437
|
-
|
|
438
|
-
MIT
|
|
1
|
+
# NextLimiter
|
|
2
|
+
|
|
3
|
+
**Production-ready rate limiting for Node.js — simple, smart, and built for real SaaS apps.**
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/nextlimiter)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[]()
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Why NextLimiter?
|
|
12
|
+
|
|
13
|
+
Most rate limiting libraries make you choose between simple-but-limited and powerful-but-complex. NextLimiter does both.
|
|
14
|
+
|
|
15
|
+
| Feature | express-rate-limit | rate-limiter-flexible | **NextLimiter** |
|
|
16
|
+
|---|---|---|---|
|
|
17
|
+
| Zero-config usage | ✓ | ✗ | ✓ |
|
|
18
|
+
| SaaS plan tiers | ✗ | ✗ | **✓** |
|
|
19
|
+
| Smart / behavior-based limiting | ✗ | ✗ | **✓** |
|
|
20
|
+
| Built-in analytics | ✗ | ✗ | **✓** |
|
|
21
|
+
| Programmatic `check()` API | ✗ | ✓ | ✓ |
|
|
22
|
+
| Named presets | ✗ | ✗ | **✓** |
|
|
23
|
+
| TypeScript types included | ✓ | ✓ | ✓ |
|
|
24
|
+
| Zero dependencies | ✓ | ✗ | ✓ |
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install nextlimiter
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
No Redis required. Works out of the box with in-memory storage.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### Zero-config (one line)
|
|
41
|
+
|
|
42
|
+
```js
|
|
43
|
+
const { autoLimit } = require('nextlimiter');
|
|
44
|
+
app.use(autoLimit());
|
|
45
|
+
// → 100 requests/min per IP, sliding window, no setup needed
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Custom configuration
|
|
49
|
+
|
|
50
|
+
```js
|
|
51
|
+
const { createLimiter } = require('nextlimiter');
|
|
52
|
+
|
|
53
|
+
const limiter = createLimiter({
|
|
54
|
+
windowMs: 60_000, // 1 minute
|
|
55
|
+
max: 100, // max 100 requests per window
|
|
56
|
+
strategy: 'sliding-window',
|
|
57
|
+
logging: true,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
app.use('/api', limiter.middleware());
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Core Concepts
|
|
66
|
+
|
|
67
|
+
### Strategies
|
|
68
|
+
|
|
69
|
+
#### `sliding-window` (default)
|
|
70
|
+
The most accurate algorithm. Uses a weighted two-window approximation — same approach as Cloudflare and Nginx's `limit_req_zone`. No boundary-burst problem. O(1) memory per key.
|
|
71
|
+
|
|
72
|
+
```js
|
|
73
|
+
createLimiter({ strategy: 'sliding-window', windowMs: 60_000, max: 100 })
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### `token-bucket`
|
|
77
|
+
Tokens refill continuously. Allows controlled bursts up to `max` tokens while enforcing a sustained rate. Used by Stripe for their API. Best for APIs where occasional spikes are expected.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
createLimiter({ strategy: 'token-bucket', windowMs: 60_000, max: 100 })
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
#### `fixed-window`
|
|
84
|
+
Simplest approach. Counts requests in fixed time intervals. Lowest memory usage. Note: susceptible to boundary-burst attacks (a client can use 2× the limit by straddling a window boundary).
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
createLimiter({ strategy: 'fixed-window', windowMs: 60_000, max: 100 })
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Features
|
|
93
|
+
|
|
94
|
+
### SaaS Plan-Based Limiting
|
|
95
|
+
|
|
96
|
+
Apply different rate limits based on subscription tier without writing conditional logic:
|
|
97
|
+
|
|
98
|
+
```js
|
|
99
|
+
const { createPlanLimiter } = require('nextlimiter');
|
|
100
|
+
|
|
101
|
+
// Built-in plans: free (60/min), pro (600/min), enterprise (6000/min)
|
|
102
|
+
const limiter = createPlanLimiter('pro', {
|
|
103
|
+
keyBy: 'api-key',
|
|
104
|
+
logging: true,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
app.use('/api', limiter.middleware());
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Custom plan definitions:**
|
|
111
|
+
|
|
112
|
+
```js
|
|
113
|
+
const limiter = createLimiter({
|
|
114
|
+
plans: {
|
|
115
|
+
startup: { windowMs: 60_000, max: 150, burstMax: 20 },
|
|
116
|
+
growth: { windowMs: 60_000, max: 500, burstMax: 80 },
|
|
117
|
+
enterprise: { windowMs: 60_000, max: 5000, burstMax: 500 },
|
|
118
|
+
},
|
|
119
|
+
plan: 'startup', // swap this based on req.user.plan at runtime
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
**Dynamic plan selection per request:**
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
// Create a limiter per plan, pick the right one in your route handler
|
|
127
|
+
const planLimiters = {
|
|
128
|
+
free: createPlanLimiter('free', { keyBy: 'api-key' }),
|
|
129
|
+
pro: createPlanLimiter('pro', { keyBy: 'api-key' }),
|
|
130
|
+
enterprise: createPlanLimiter('enterprise', { keyBy: 'api-key' }),
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
app.use('/api', (req, res, next) => {
|
|
134
|
+
const plan = req.user?.plan || 'free';
|
|
135
|
+
return planLimiters[plan].middleware()(req, res, next);
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### Smart Rate Limiting
|
|
142
|
+
|
|
143
|
+
Detects burst traffic and dynamically reduces limits for suspicious clients — without blocking them entirely.
|
|
144
|
+
|
|
145
|
+
```js
|
|
146
|
+
const limiter = createLimiter({
|
|
147
|
+
windowMs: 60_000,
|
|
148
|
+
max: 100,
|
|
149
|
+
smart: true,
|
|
150
|
+
smartThreshold: 2.0, // flag if rate exceeds 2× normal
|
|
151
|
+
smartCooldownMs: 60_000, // penalty lasts 60 seconds
|
|
152
|
+
smartPenaltyFactor: 0.5, // reduce limit to 50% during penalty
|
|
153
|
+
});
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**How it works:**
|
|
157
|
+
1. Tracks the request rate for each key in a short observation window (10% of `windowMs`)
|
|
158
|
+
2. If rate exceeds `normalRate × smartThreshold`, the key is flagged
|
|
159
|
+
3. Flagged keys get `floor(max × smartPenaltyFactor)` as their effective limit
|
|
160
|
+
4. Penalty expires after `smartCooldownMs`
|
|
161
|
+
5. Legitimate users are completely unaffected
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
### Named Presets
|
|
166
|
+
|
|
167
|
+
Four built-in presets for the most common scenarios:
|
|
168
|
+
|
|
169
|
+
```js
|
|
170
|
+
const { createPresetLimiter } = require('nextlimiter');
|
|
171
|
+
|
|
172
|
+
// Strict — 30 req/min, sliding window, smart limiting on
|
|
173
|
+
app.use('/admin', createPresetLimiter('strict').middleware());
|
|
174
|
+
|
|
175
|
+
// Relaxed — 300 req/min, token bucket
|
|
176
|
+
app.use('/public', createPresetLimiter('relaxed').middleware());
|
|
177
|
+
|
|
178
|
+
// API — 100 req/min, api-key based, smart on
|
|
179
|
+
app.use('/api', createPresetLimiter('api').middleware());
|
|
180
|
+
|
|
181
|
+
// Auth — 10 attempts per 15 minutes (brute-force protection)
|
|
182
|
+
app.post('/login', createPresetLimiter('auth').middleware());
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
### Built-in Analytics
|
|
188
|
+
|
|
189
|
+
Every limiter instance tracks metrics automatically:
|
|
190
|
+
|
|
191
|
+
```js
|
|
192
|
+
const stats = limiter.getStats();
|
|
193
|
+
|
|
194
|
+
console.log(stats);
|
|
195
|
+
// {
|
|
196
|
+
// totalRequests: 15420,
|
|
197
|
+
// blockedRequests: 234,
|
|
198
|
+
// allowedRequests: 15186,
|
|
199
|
+
// blockRate: 0.0152,
|
|
200
|
+
// topKeys: [
|
|
201
|
+
// { key: 'nextlimiter:ip:1.2.3.4', count: 892 },
|
|
202
|
+
// { key: 'nextlimiter:ip:5.6.7.8', count: 441 },
|
|
203
|
+
// ],
|
|
204
|
+
// topBlocked: [
|
|
205
|
+
// { key: 'nextlimiter:ip:1.2.3.4', count: 78 },
|
|
206
|
+
// ],
|
|
207
|
+
// trackedSince: '2024-01-15T10:00:00.000Z',
|
|
208
|
+
// uptimeMs: 3600000,
|
|
209
|
+
// config: {
|
|
210
|
+
// strategy: 'sliding-window',
|
|
211
|
+
// windowMs: 60000,
|
|
212
|
+
// max: 100,
|
|
213
|
+
// plan: 'pro',
|
|
214
|
+
// smart: true,
|
|
215
|
+
// }
|
|
216
|
+
// }
|
|
217
|
+
|
|
218
|
+
// Expose as an endpoint (protect with auth in production)
|
|
219
|
+
app.get('/admin/stats', (req, res) => res.json(limiter.getStats()));
|
|
220
|
+
|
|
221
|
+
// Reset counters
|
|
222
|
+
limiter.resetStats();
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
### Key-Based Limiting
|
|
228
|
+
|
|
229
|
+
**By IP (default):**
|
|
230
|
+
```js
|
|
231
|
+
createLimiter({ keyBy: 'ip' })
|
|
232
|
+
// Uses X-Forwarded-For → X-Real-IP → req.ip (proxy-aware)
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
**By authenticated user ID:**
|
|
236
|
+
```js
|
|
237
|
+
createLimiter({ keyBy: 'user-id' })
|
|
238
|
+
// Reads req.user.id → req.user._id → req.userId
|
|
239
|
+
// Falls back to IP for unauthenticated requests
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**By API key:**
|
|
243
|
+
```js
|
|
244
|
+
createLimiter({ keyBy: 'api-key' })
|
|
245
|
+
// Reads Authorization: Bearer <token> → X-API-Key header → ?apiKey query param
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
**Custom key function:**
|
|
249
|
+
```js
|
|
250
|
+
createLimiter({
|
|
251
|
+
keyGenerator: (req) => `tenant:${req.headers['x-tenant-id']}`,
|
|
252
|
+
})
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
---
|
|
256
|
+
|
|
257
|
+
### Programmatic API
|
|
258
|
+
|
|
259
|
+
Use `limiter.check()` for rate limiting outside HTTP middleware — WebSockets, background jobs, cron tasks:
|
|
260
|
+
|
|
261
|
+
```js
|
|
262
|
+
const limiter = createLimiter({ windowMs: 60_000, max: 10 });
|
|
263
|
+
|
|
264
|
+
// WebSocket message handler
|
|
265
|
+
async function onMessage(userId, message) {
|
|
266
|
+
const result = await limiter.check(`ws:${userId}`);
|
|
267
|
+
|
|
268
|
+
if (!result.allowed) {
|
|
269
|
+
socket.emit('error', {
|
|
270
|
+
message: 'Rate limit exceeded',
|
|
271
|
+
retryAfter: result.retryAfter,
|
|
272
|
+
});
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
processMessage(message);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Background job
|
|
280
|
+
async function runExport(userId) {
|
|
281
|
+
const result = await limiter.check(`export:${userId}`);
|
|
282
|
+
if (!result.allowed) throw new Error(`Try again in ${result.retryAfter}s`);
|
|
283
|
+
// ... run export
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### Developer-Friendly Logging
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
createLimiter({ logging: true, logPrefix: '[API]' })
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Output:
|
|
296
|
+
```
|
|
297
|
+
2024-01-15T10:23:41.000Z [API] BLOCKED ip:1.2.3.4 (101/100) via sliding-window
|
|
298
|
+
2024-01-15T10:23:42.000Z [API] BLOCKED ip:1.2.3.4 (45/22) via sliding-window [smart]
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Colors are automatically disabled in non-TTY environments (CI, Docker logs).
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
### Skip and Custom Handlers
|
|
306
|
+
|
|
307
|
+
```js
|
|
308
|
+
createLimiter({
|
|
309
|
+
// Skip rate limiting for specific requests
|
|
310
|
+
skip: (req) =>
|
|
311
|
+
req.path === '/health' ||
|
|
312
|
+
req.headers['x-internal-service'] === 'true' ||
|
|
313
|
+
req.ip === '127.0.0.1',
|
|
314
|
+
|
|
315
|
+
// Full control over the blocked response
|
|
316
|
+
onLimitReached: (req, res, result) => {
|
|
317
|
+
res.status(429).json({
|
|
318
|
+
error: 'Rate limit exceeded',
|
|
319
|
+
retryAfter: result.retryAfter,
|
|
320
|
+
upgrade: 'Upgrade to Pro for 10× the rate limit',
|
|
321
|
+
docsUrl: 'https://yourapp.com/docs/rate-limits',
|
|
322
|
+
});
|
|
323
|
+
},
|
|
324
|
+
})
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Full Configuration Reference
|
|
330
|
+
|
|
331
|
+
| Option | Type | Default | Description |
|
|
332
|
+
|---|---|---|---|
|
|
333
|
+
| `windowMs` | `number` | `60000` | Time window in milliseconds |
|
|
334
|
+
| `max` | `number` | `100` | Max requests per window |
|
|
335
|
+
| `strategy` | `string` | `'sliding-window'` | `'fixed-window'` \| `'sliding-window'` \| `'token-bucket'` |
|
|
336
|
+
| `keyBy` | `string\|fn` | `'ip'` | `'ip'` \| `'user-id'` \| `'api-key'` \| `(req) => string` |
|
|
337
|
+
| `keyPrefix` | `string` | `'nextlimiter:'` | Redis/store key prefix |
|
|
338
|
+
| `plan` | `string` | `null` | `'free'` \| `'pro'` \| `'enterprise'` |
|
|
339
|
+
| `plans` | `object` | built-in | Custom plan definitions |
|
|
340
|
+
| `preset` | `string` | `null` | `'strict'` \| `'relaxed'` \| `'api'` \| `'auth'` |
|
|
341
|
+
| `smart` | `boolean` | `false` | Enable smart burst detection |
|
|
342
|
+
| `smartThreshold` | `number` | `2.0` | Rate multiplier that triggers penalty |
|
|
343
|
+
| `smartCooldownMs` | `number` | `60000` | How long smart penalty lasts |
|
|
344
|
+
| `smartPenaltyFactor` | `number` | `0.5` | Limit multiplier during penalty (0–1) |
|
|
345
|
+
| `logging` | `boolean` | `false` | Enable console logging |
|
|
346
|
+
| `logPrefix` | `string` | `'[NextLimiter]'` | Log line prefix |
|
|
347
|
+
| `headers` | `boolean` | `true` | Send `X-RateLimit-*` headers |
|
|
348
|
+
| `statusCode` | `number` | `429` | HTTP status for blocked requests |
|
|
349
|
+
| `message` | `string` | `'Too many requests...'` | Default 429 message |
|
|
350
|
+
| `store` | `Store` | `MemoryStore` | Custom storage backend |
|
|
351
|
+
| `skip` | `fn` | `null` | `(req) => boolean` — skip rate limiting |
|
|
352
|
+
| `onLimitReached` | `fn` | `null` | `(req, res, result) => void` |
|
|
353
|
+
| `keyGenerator` | `fn` | `null` | `(req) => string` — override key generation |
|
|
354
|
+
|
|
355
|
+
---
|
|
356
|
+
|
|
357
|
+
## Response Headers
|
|
358
|
+
|
|
359
|
+
Every response includes these headers:
|
|
360
|
+
|
|
361
|
+
```
|
|
362
|
+
X-RateLimit-Limit: 100
|
|
363
|
+
X-RateLimit-Remaining: 43
|
|
364
|
+
X-RateLimit-Reset: 1705315200
|
|
365
|
+
X-RateLimit-Strategy: sliding-window
|
|
366
|
+
Retry-After: 47 ← only on 429 responses
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## Custom Store (Redis example)
|
|
372
|
+
|
|
373
|
+
Implement the `Store` interface to use any backend:
|
|
374
|
+
|
|
375
|
+
```js
|
|
376
|
+
const Redis = require('ioredis');
|
|
377
|
+
|
|
378
|
+
class RedisStore {
|
|
379
|
+
constructor(client) {
|
|
380
|
+
this.client = client;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async get(key) {
|
|
384
|
+
const val = await this.client.get(key);
|
|
385
|
+
return val ? JSON.parse(val) : undefined;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async set(key, value, ttlMs) {
|
|
389
|
+
await this.client.set(key, JSON.stringify(value), 'PX', ttlMs);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
async increment(key, ttlMs) {
|
|
393
|
+
const count = await this.client.incr(key);
|
|
394
|
+
if (count === 1) await this.client.pexpire(key, ttlMs);
|
|
395
|
+
return count;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
async delete(key) {
|
|
399
|
+
await this.client.del(key);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
keys() { return []; } // optional
|
|
403
|
+
clear() {} // optional
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Use it:
|
|
407
|
+
const limiter = createLimiter({
|
|
408
|
+
store: new RedisStore(new Redis()),
|
|
409
|
+
windowMs: 60_000,
|
|
410
|
+
max: 100,
|
|
411
|
+
});
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
---
|
|
415
|
+
|
|
416
|
+
## TypeScript
|
|
417
|
+
|
|
418
|
+
Full TypeScript support included — no `@types/nextlimiter` needed:
|
|
419
|
+
|
|
420
|
+
```ts
|
|
421
|
+
import { createLimiter, LimiterOptions, RateLimitResult, Store } from 'nextlimiter';
|
|
422
|
+
|
|
423
|
+
const options: LimiterOptions = {
|
|
424
|
+
windowMs: 60_000,
|
|
425
|
+
max: 100,
|
|
426
|
+
strategy: 'token-bucket',
|
|
427
|
+
smart: true,
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const limiter = createLimiter(options);
|
|
431
|
+
const result: RateLimitResult = await limiter.check('user:42');
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
---
|
|
435
|
+
|
|
436
|
+
## License
|
|
437
|
+
|
|
438
|
+
MIT
|