halt-rate 0.2.0 → 0.4.0

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.
Files changed (38) hide show
  1. package/README.md +34 -921
  2. package/dist/adapters/express.d.mts +1 -1
  3. package/dist/adapters/express.d.ts +1 -1
  4. package/dist/adapters/fastify.d.mts +29 -0
  5. package/dist/adapters/fastify.d.ts +29 -0
  6. package/dist/adapters/fastify.js +38 -0
  7. package/dist/adapters/fastify.js.map +1 -0
  8. package/dist/adapters/fastify.mjs +35 -0
  9. package/dist/adapters/fastify.mjs.map +1 -0
  10. package/dist/adapters/graphql.d.mts +22 -0
  11. package/dist/adapters/graphql.d.ts +22 -0
  12. package/dist/adapters/graphql.js +51 -0
  13. package/dist/adapters/graphql.js.map +1 -0
  14. package/dist/adapters/graphql.mjs +49 -0
  15. package/dist/adapters/graphql.mjs.map +1 -0
  16. package/dist/adapters/hono.d.mts +30 -0
  17. package/dist/adapters/hono.d.ts +30 -0
  18. package/dist/adapters/hono.js +56 -0
  19. package/dist/adapters/hono.js.map +1 -0
  20. package/dist/adapters/hono.mjs +54 -0
  21. package/dist/adapters/hono.mjs.map +1 -0
  22. package/dist/adapters/next.d.mts +1 -1
  23. package/dist/adapters/next.d.ts +1 -1
  24. package/dist/adapters/next.js +26 -3
  25. package/dist/adapters/next.js.map +1 -1
  26. package/dist/adapters/next.mjs +26 -3
  27. package/dist/adapters/next.mjs.map +1 -1
  28. package/dist/index.d.mts +174 -3
  29. package/dist/index.d.ts +174 -3
  30. package/dist/index.js +631 -3
  31. package/dist/index.js.map +1 -1
  32. package/dist/index.mjs +614 -4
  33. package/dist/index.mjs.map +1 -1
  34. package/dist/limiter-DqVVE0Kl.d.mts +310 -0
  35. package/dist/limiter-DqVVE0Kl.d.ts +310 -0
  36. package/package.json +80 -13
  37. package/dist/limiter-HCt8ADZ2.d.mts +0 -177
  38. package/dist/limiter-HCt8ADZ2.d.ts +0 -177
package/README.md CHANGED
@@ -1,949 +1,62 @@
1
- # Halt TypeScript SDK
1
+ # Halt TypeScript SDK
2
2
 
3
- **Drop-in middleware that enforces consistent rate limits per IP/user/api-key with safe defaults, Redis-backed accuracy, and clean headers.**
3
+ **SaaS-aware, cross-language rate limiting.** Per-user / per-API-key / per-plan limits, quotas, weighted endpoints, abuse controls, atomic Redis accuracy, and built-in observability — with a matching [Python package](https://pypi.org/project/halt-rate/).
4
4
 
5
+ [![npm](https://img.shields.io/npm/v/halt-rate.svg)](https://www.npmjs.com/package/halt-rate)
5
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
6
- [![TypeScript](https://img.shields.io/badge/TypeScript-5.0+-blue.svg)](https://www.typescriptlang.org/)
7
7
 
8
- ## Features
9
-
10
- 🚀 **Four Rate Limiting Algorithms**
11
- - Token Bucket (burst-friendly, recommended)
12
- - Fixed Window (simple, fast)
13
- - Sliding Window (accurate, memory-intensive)
14
- - Leaky Bucket (traffic shaping, constant rate)
15
-
16
- 💾 **Multiple Storage Backends**
17
- - In-Memory (development, single-threaded)
18
- - Redis (production, distributed, **atomic via Lua**, cluster-safe) ✅
19
- - PostgreSQL (ACID, relational)
20
- - MongoDB (document store, TTL indexes)
21
- - DynamoDB (AWS serverless, auto-scaling)
22
- - Memcached (distributed cache, fast)
23
-
24
- 🎯 **SaaS-Ready Features**
25
- - Plan-based rate limiting (FREE, STARTER, PRO, BUSINESS, ENTERPRISE)
26
- - Quota management (hourly, daily, monthly, yearly)
27
- - Penalty system (abuse detection, progressive penalties)
28
- - Telemetry hooks (logging, metrics, observability)
29
-
30
- 🔧 **Framework Support**
31
- - Express
32
- - Next.js (App Router & Pages Router)
33
- - Next.js Middleware
34
-
35
- ✨ **Smart Features**
36
- - Automatic health check exemptions
37
- - Private IP exemptions
38
- - Custom exemption lists
39
- - Weighted endpoints (cost-based limiting)
40
- - Per-request algorithm override
41
- - Standard rate limit headers (RateLimit-*, Retry-After)
42
-
43
- ---
44
-
45
- ## Installation
46
-
47
- ```bash
48
- npm install halt
49
- # or
50
- yarn add halt
51
- # or
52
- pnpm add halt
53
- ```
8
+ 📖 **Full documentation: [halt.afroawi.com](https://halt.afroawi.com)**
54
9
 
55
- ### Optional Dependencies
10
+ ## Install
56
11
 
57
12
  ```bash
58
- # Redis support (recommended for production)
59
- npm install ioredis
60
-
61
- # PostgreSQL support
62
- npm install pg
63
-
64
- # MongoDB support
65
- npm install mongodb
66
-
67
- # DynamoDB support
68
- npm install @aws-sdk/client-dynamodb @aws-sdk/util-dynamodb
69
-
70
- # Memcached support
71
- npm install memcached
72
- ```
73
-
74
- ---
75
-
76
- ## Storage Backends
77
-
78
- ### In-Memory (Development)
79
-
80
- ```typescript
81
- import { InMemoryStore } from 'halt';
82
-
83
- const store = new InMemoryStore();
84
- ```
85
-
86
- ### Redis (Production, recommended)
87
-
88
- The Redis store runs the full check-and-consume **inside Redis via a Lua script**,
89
- so limits stay accurate under concurrent load from any number of app servers. You
90
- inject your own client (ioredis or node-redis v4+), so Halt has no hard dependency
91
- on a particular Redis library.
92
-
93
- ```typescript
94
- import Redis from 'ioredis';
95
- import { RateLimiter, RedisStore, presets } from 'halt';
96
-
97
- const store = new RedisStore({
98
- client: new Redis(process.env.REDIS_URL),
99
- // What to do if Redis is unreachable:
100
- // 'open' (default) — allow the request (don't take down traffic)
101
- // 'closed' — block the request (429)
102
- failMode: 'open',
103
- onError: (err) => console.error('redis rate-limit error', err),
104
- });
105
-
106
- const limiter = new RateLimiter({ store, policy: presets.PUBLIC_API });
107
- ```
108
-
109
- **Atomicity.** All four algorithms are implemented as single-key Lua scripts, so a
110
- burst of concurrent requests can never over-admit past the limit. The script reads
111
- the clock from the Redis server (`TIME`), so app-server clock skew doesn't matter.
112
-
113
- **Redis Cluster.** Every script touches exactly one key, so it is cluster-safe out
114
- of the box. To colocate related keys on the same slot, wrap the variable part of
115
- your key in a hash tag, e.g. `halt:{user-123}:...`.
116
-
117
- > JavaScript has no synchronous network I/O, so the TypeScript store is async-only
118
- > (the limiter's `check()` is already async). A synchronous client is available in
119
- > the Python package.
120
-
121
- ### PostgreSQL
122
-
123
- ```typescript
124
- import { PostgresStore } from 'halt/stores/postgres';
125
-
126
- const store = new PostgresStore({
127
- host: 'localhost',
128
- port: 5432,
129
- database: 'mydb',
130
- user: 'user',
131
- password: 'password',
132
- tableName: 'rate_limits', // optional
133
- });
134
- ```
135
-
136
- ### MongoDB
137
-
138
- ```typescript
139
- import { MongoDBStore } from 'halt/stores/mongodb';
140
-
141
- const store = new MongoDBStore({
142
- connectionString: 'mongodb://localhost:27017',
143
- database: 'halt',
144
- collection: 'rate_limits',
145
- });
146
- ```
147
-
148
- ### DynamoDB
149
-
150
- ```typescript
151
- import { DynamoDBStore } from 'halt/stores/dynamodb';
152
-
153
- const store = new DynamoDBStore({
154
- tableName: 'rate_limits',
155
- region: 'us-east-1',
156
- });
157
- ```
158
-
159
- ### Memcached
160
-
161
- ```typescript
162
- import { MemcachedStore } from 'halt/stores/memcached';
163
-
164
- const store = new MemcachedStore({
165
- servers: 'localhost:11211',
166
- });
167
- ```
168
-
169
- ---
170
-
171
- ## SaaS Features
172
-
173
- ### Plan-Based Rate Limiting
174
-
175
- ```typescript
176
- import { getPlanPolicy, PLAN_FREE, PLAN_PRO, PLAN_ENTERPRISE } from 'halt';
177
-
178
- // Use plan-based presets
179
- const freePolicy = PLAN_FREE; // 100 req/hour
180
- const proPolicy = PLAN_PRO; // 2000 req/hour
181
- const enterprisePolicy = PLAN_ENTERPRISE; // 20000 req/hour
182
-
183
- // Get policy by plan name
184
- const policy = getPlanPolicy('pro');
185
-
186
- // Dynamic policy resolution
187
- function getUserPolicy(user: User) {
188
- return getPlanPolicy(user.plan);
189
- }
190
- ```
191
-
192
- ### Quota Management
193
-
194
- ```typescript
195
- import { QuotaManager, Quota, QuotaPeriod } from 'halt/core/quota';
196
-
197
- const quotaManager = new QuotaManager(store);
198
-
199
- const monthlyQuota: Quota = {
200
- name: 'api_calls',
201
- limit: 100000,
202
- period: QuotaPeriod.MONTHLY,
203
- };
204
-
205
- // Check quota
206
- const { allowed, quota: currentQuota } = await quotaManager.checkQuota(
207
- 'user_123',
208
- monthlyQuota
209
- );
210
-
211
- if (allowed) {
212
- // Consume quota
213
- await quotaManager.consumeQuota('user_123', monthlyQuota, 1);
214
- } else {
215
- console.log(`Quota exceeded. Resets at: ${currentQuota.resetAt}`);
216
- }
217
- ```
218
-
219
- ### Penalty System
220
-
221
- ```typescript
222
- import { PenaltyManager, PENALTY_MODERATE } from 'halt/core/penalty';
223
-
224
- const penaltyManager = new PenaltyManager(store, PENALTY_MODERATE);
225
-
226
- // Record violation
227
- const penalty = await penaltyManager.recordViolation('user_123', 1.0);
228
-
229
- // Check penalty status
230
- if (penaltyManager.isActive(penalty)) {
231
- console.log(`User penalized until: ${penalty.penaltyUntil}`);
232
- console.log(`Abuse score: ${penalty.abuseScore}`);
233
- }
234
- ```
235
-
236
- ### Telemetry & Observability
237
-
238
- ```typescript
239
- import { LoggingTelemetry, MetricsTelemetry, CompositeTelemetry } from 'halt/core/telemetry';
240
-
241
- // Logging telemetry
242
- const telemetry = new LoggingTelemetry(console);
243
-
244
- // Metrics telemetry (with your metrics client)
245
- class CustomMetrics {
246
- increment(metric: string, tags?: any) { /* ... */ }
247
- gauge(metric: string, value: number, tags?: any) { /* ... */ }
248
- }
249
-
250
- const metricsTelemetry = new MetricsTelemetry(new CustomMetrics());
251
-
252
- // Combine multiple telemetry hooks
253
- const compositeTelemetry = new CompositeTelemetry([
254
- new LoggingTelemetry(console),
255
- metricsTelemetry,
256
- ]);
257
-
258
- // Use with limiter
259
- const limiter = new RateLimiter({
260
- store,
261
- policy,
262
- telemetry: compositeTelemetry,
263
- });
264
- ```
265
-
266
- ---
267
-
268
- ## Quick Start
269
-
270
- ### Express
271
-
272
- ```typescript
273
- import express from 'express';
274
- import { RateLimiter, InMemoryStore, presets } from 'halt';
275
- import { haltMiddleware } from 'halt/express';
276
-
277
- const app = express();
278
-
279
- const limiter = new RateLimiter({
280
- store: new InMemoryStore(),
281
- policy: presets.PUBLIC_API, // 100 req/min
282
- });
283
-
284
- app.use(haltMiddleware({ limiter }));
285
-
286
- app.get('/', (req, res) => {
287
- res.json({ message: 'Hello World' });
288
- });
289
-
290
- app.listen(3000);
291
- ```
292
-
293
- ### Next.js App Router
294
-
295
- ```typescript
296
- // app/api/data/route.ts
297
- import { withHalt } from 'halt/next';
298
- import { InMemoryStore, presets } from 'halt';
299
-
300
- const store = new InMemoryStore();
301
-
302
- async function handler(req: Request) {
303
- return Response.json({ message: 'Hello World' });
304
- }
305
-
306
- export const GET = withHalt(handler, {
307
- store,
308
- policy: presets.PUBLIC_API,
309
- });
310
- ```
311
-
312
- ### Next.js Middleware
313
-
314
- ```typescript
315
- // middleware.ts
316
- import { haltMiddleware } from 'halt/next';
317
- import { InMemoryStore, presets } from 'halt';
318
-
319
- export default haltMiddleware({
320
- store: new InMemoryStore(),
321
- policy: presets.PUBLIC_API,
322
- });
323
-
324
- export const config = {
325
- matcher: '/api/:path*',
326
- };
327
- ```
328
-
329
- ---
330
-
331
- ## Preset Policies
332
-
333
- Halt comes with battle-tested presets:
334
-
335
- ```typescript
336
- import { presets } from 'halt';
337
-
338
- // Public API - moderate limits
339
- presets.PUBLIC_API
340
- // 100 requests/minute, burst: 120
341
-
342
- // Authentication endpoints - strict
343
- presets.AUTH_ENDPOINTS
344
- // 5 requests/minute, burst: 10, 5min cooldown
345
-
346
- // Expensive operations - very strict
347
- presets.EXPENSIVE_OPS
348
- // 10 requests/hour, burst: 15, cost: 10
349
-
350
- // Strict API - for sensitive ops
351
- presets.STRICT_API
352
- // 20 requests/minute, burst: 25
353
-
354
- // Generous API - for internal services
355
- presets.GENEROUS_API
356
- // 1000 requests/minute, burst: 1200
357
- ```
358
-
359
- ---
360
-
361
- ## Custom Policies
362
-
363
- ### Basic Custom Policy
364
-
365
- ```typescript
366
- import { Policy, KeyStrategy, Algorithm } from 'halt';
367
-
368
- const customPolicy: Policy = {
369
- name: 'custom',
370
- limit: 50,
371
- window: 60, // 1 minute
372
- burst: 60,
373
- algorithm: Algorithm.TOKEN_BUCKET,
374
- keyStrategy: KeyStrategy.IP,
375
- };
376
- ```
377
-
378
- ### Advanced Examples
379
-
380
- #### Rate Limit by User
381
-
382
- ```typescript
383
- const userPolicy: Policy = {
384
- name: 'per_user',
385
- limit: 100,
386
- window: 3600, // 1 hour
387
- keyStrategy: KeyStrategy.USER,
388
- };
389
- ```
390
-
391
- #### Rate Limit by API Key
392
-
393
- ```typescript
394
- const apiPolicy: Policy = {
395
- name: 'per_api_key',
396
- limit: 1000,
397
- window: 60,
398
- keyStrategy: KeyStrategy.API_KEY,
399
- };
400
- ```
401
-
402
- #### Composite Keys (User + IP)
403
-
404
- ```typescript
405
- const compositePolicy: Policy = {
406
- name: 'user_and_ip',
407
- limit: 50,
408
- window: 60,
409
- keyStrategy: KeyStrategy.COMPOSITE,
410
- };
411
- ```
412
-
413
- #### Weighted Endpoints
414
-
415
- ```typescript
416
- const expensivePolicy: Policy = {
417
- name: 'llm_endpoint',
418
- limit: 100,
419
- window: 3600,
420
- cost: 10, // Each request costs 10 tokens
421
- algorithm: Algorithm.TOKEN_BUCKET,
422
- };
13
+ npm install halt-rate
14
+ # optional: production store + metrics
15
+ npm install ioredis @opentelemetry/api
423
16
  ```
424
17
 
425
- ---
426
-
427
- ## Algorithms
428
-
429
- ### Token Bucket (Recommended)
430
-
431
- Best for most use cases. Handles bursts naturally while maintaining average rate.
432
-
433
- ```typescript
434
- import { Policy, Algorithm } from 'halt';
435
-
436
- const policy: Policy = {
437
- name: 'token_bucket',
438
- limit: 100, // 100 tokens per window
439
- window: 60, // 1 minute
440
- burst: 120, // Allow bursts up to 120
441
- algorithm: Algorithm.TOKEN_BUCKET,
442
- };
443
- ```
444
-
445
- **Pros:**
446
- - ✅ Handles burst traffic naturally
447
- - ✅ Smooth rate limiting
448
- - ✅ Low memory usage
449
-
450
- **Cons:**
451
- - ❌ Slightly more complex than fixed window
452
-
453
- ### Fixed Window
454
-
455
- Simple and fast. Good for strict limits.
18
+ ## Quick start
456
19
 
457
20
  ```typescript
458
- const policy: Policy = {
459
- name: 'fixed_window',
460
- limit: 100,
461
- window: 60,
462
- algorithm: Algorithm.FIXED_WINDOW,
463
- };
464
- ```
465
-
466
- **Pros:**
467
- - ✅ Very simple
468
- - ✅ Low memory usage
469
- - ✅ Fast
470
-
471
- **Cons:**
472
- - ❌ Can allow 2x limit at window boundaries
473
- - ❌ No burst handling
474
-
475
- ### Sliding Window
476
-
477
- Most accurate but uses more memory.
478
-
479
- ```typescript
480
- const policy: Policy = {
481
- name: 'sliding_window',
482
- limit: 100,
483
- window: 60,
484
- algorithm: Algorithm.SLIDING_WINDOW,
485
- };
486
- ```
487
-
488
- **Pros:**
489
- - ✅ Most accurate
490
- - ✅ No boundary issues
491
-
492
- **Cons:**
493
- - ❌ Higher memory usage
494
- - ❌ Slightly slower
21
+ import { RateLimiter, InMemoryStore, presets } from 'halt-rate';
495
22
 
496
- ### Leaky Bucket
497
-
498
- Traffic shaping with constant processing rate.
499
-
500
- ```typescript
501
- const policy: Policy = {
502
- name: 'leaky_bucket',
503
- limit: 100,
504
- window: 60,
505
- burst: 120,
506
- algorithm: Algorithm.LEAKY_BUCKET,
507
- };
508
- ```
509
-
510
- **Pros:**
511
- - ✅ Smooth traffic shaping
512
- - ✅ Predictable behavior
513
-
514
- **Cons:**
515
- - ❌ May delay legitimate bursts
516
-
517
- **Use case:** Strict QoS requirements, traffic shaping
518
-
519
- ---
520
-
521
- ## Key Strategies
522
-
523
- ### IP-based (Default)
524
-
525
- ```typescript
526
- import { Policy, KeyStrategy, RateLimiter } from 'halt';
527
-
528
- const policy: Policy = {
529
- name: 'per_ip',
530
- limit: 100,
531
- window: 60,
532
- keyStrategy: KeyStrategy.IP,
533
- };
534
-
535
- // With trusted proxies (for X-Forwarded-For)
536
23
  const limiter = new RateLimiter({
537
- store,
538
- policy,
539
- trustedProxies: ['10.0.0.0/8', '172.16.0.0/12'],
24
+ store: new InMemoryStore(), // use RedisStore in production
25
+ policy: presets.PUBLIC_API, // 100 req/min per IP
540
26
  });
541
- ```
542
-
543
- ### User-based
544
-
545
- ```typescript
546
- const policy: Policy = {
547
- name: 'per_user',
548
- limit: 1000,
549
- window: 3600,
550
- keyStrategy: KeyStrategy.USER,
551
- };
552
- ```
553
-
554
- Extracts user ID from:
555
- - `request.user.id`
556
- - `request.userId`
557
27
 
558
- ### API Key-based
559
-
560
- ```typescript
561
- const policy: Policy = {
562
- name: 'per_api_key',
563
- limit: 5000,
564
- window: 3600,
565
- keyStrategy: KeyStrategy.API_KEY,
566
- };
567
- ```
568
-
569
- Extracts API key from headers:
570
- - `X-API-Key`
571
- - `Authorization` (including Bearer tokens)
572
-
573
- ### Custom Key Extraction
574
-
575
- ```typescript
576
- function extractOrgId(request: any): string | null {
577
- return request.headers['x-organization-id'] || null;
28
+ const decision = await limiter.check(req);
29
+ if (!decision.allowed) {
30
+ // respond 429, Retry-After: decision.retryAfter
578
31
  }
579
-
580
- const policy: Policy = {
581
- name: 'per_org',
582
- limit: 10000,
583
- window: 3600,
584
- keyStrategy: KeyStrategy.CUSTOM,
585
- keyExtractor: extractOrgId,
586
- };
587
- ```
588
-
589
- ---
590
-
591
- ## Exemptions
592
-
593
- ### Automatic Exemptions
594
-
595
- Halt automatically exempts:
596
-
597
- **Health Checks:**
598
- - `/health`
599
- - `/ping`
600
- - `/ready`
601
- - `/healthz`
602
- - `/livez`
603
-
604
- **Private IPs:**
605
- - `127.0.0.1` (localhost)
606
- - `10.0.0.0/8`
607
- - `172.16.0.0/12`
608
- - `192.168.0.0/16`
609
-
610
- ### Custom Exemptions
611
-
612
- ```typescript
613
- const policy: Policy = {
614
- name: 'custom',
615
- limit: 100,
616
- window: 60,
617
- exemptions: [
618
- '/admin', // Path exemption
619
- '/internal', // Another path
620
- '192.168.1.100', // IP exemption
621
- ],
622
- };
623
-
624
- // Disable private IP exemptions
625
- const limiter = new RateLimiter({
626
- store,
627
- policy,
628
- exemptPrivateIps: false,
629
- });
630
- ```
631
-
632
- ---
633
-
634
- ## Per-Route Rate Limiting
635
-
636
- ### Express - Route-Specific
637
-
638
- ```typescript
639
- import { createLimiter } from 'halt/express';
640
-
641
- const publicLimiter = new RateLimiter({ store, policy: presets.PUBLIC_API });
642
- const authLimiter = new RateLimiter({ store, policy: presets.AUTH_ENDPOINTS });
643
-
644
- app.get('/api/data', createLimiter(publicLimiter), (req, res) => {
645
- res.json({ data: '...' });
646
- });
647
-
648
- app.post('/auth/login', createLimiter(authLimiter), (req, res) => {
649
- res.json({ token: '...' });
650
- });
651
32
  ```
652
33
 
653
- ### Next.js - Multiple Policies
654
-
655
- ```typescript
656
- // app/api/data/route.ts
657
- import { withPolicy } from 'halt/next';
658
- import { InMemoryStore, presets } from 'halt';
659
-
660
- const store = new InMemoryStore();
34
+ Production Redis, Express/Next adapters, plan-based limits, quotas, penalties, and
35
+ observability are covered in the docs → **[halt.afroawi.com/docs](https://halt.afroawi.com/docs)**.
661
36
 
662
- async function handler(req: Request) {
663
- return Response.json({ data: '...' });
664
- }
665
-
666
- export const GET = withPolicy(handler, presets.PUBLIC_API, store);
667
- ```
668
-
669
- ```typescript
670
- // app/api/auth/login/route.ts
671
- import { withPolicy } from 'halt/next';
672
- import { InMemoryStore, presets } from 'halt';
673
-
674
- const store = new InMemoryStore();
675
-
676
- async function handler(req: Request) {
677
- return Response.json({ token: '...' });
678
- }
679
-
680
- export const POST = withPolicy(handler, presets.AUTH_ENDPOINTS, store);
681
- ```
682
-
683
- ---
684
-
685
- ## Response Headers
686
-
687
- All responses include standard rate limit headers:
688
-
689
- ```http
690
- HTTP/1.1 200 OK
691
- RateLimit-Limit: 100
692
- RateLimit-Remaining: 95
693
- RateLimit-Reset: 1708024800
694
- ```
695
-
696
- When rate limited (429):
697
-
698
- ```http
699
- HTTP/1.1 429 Too Many Requests
700
- RateLimit-Limit: 100
701
- RateLimit-Remaining: 0
702
- RateLimit-Reset: 1708024860
703
- Retry-After: 42
704
-
705
- {
706
- "error": "rate_limit_exceeded",
707
- "message": "Too many requests. Please try again later.",
708
- "retryAfter": 42
709
- }
710
- ```
711
-
712
- ---
713
-
714
- ## Advanced Usage
715
-
716
- ### Dynamic Cost per Request
717
-
718
- ```typescript
719
- // Next.js API Route
720
- import { RateLimiter, InMemoryStore, presets } from 'halt';
721
-
722
- const limiter = new RateLimiter({
723
- store: new InMemoryStore(),
724
- policy: presets.EXPENSIVE_OPS,
725
- });
726
-
727
- export async function POST(req: Request) {
728
- const body = await req.json();
729
- const promptLength = body.prompt?.length || 0;
730
-
731
- // Calculate cost based on request
732
- const cost = Math.max(1, Math.floor(promptLength / 100));
733
-
734
- // Check with custom cost
735
- const decision = limiter.check(req, cost);
736
-
737
- if (!decision.allowed) {
738
- return Response.json(
739
- {
740
- error: 'rate_limit_exceeded',
741
- message: 'Too many requests',
742
- retryAfter: decision.retryAfter,
743
- },
744
- { status: 429 }
745
- );
746
- }
747
-
748
- return Response.json({ response: '...' });
749
- }
750
- ```
751
-
752
- ### Multiple Policies (Express)
753
-
754
- ```typescript
755
- import express from 'express';
756
- import { RateLimiter, InMemoryStore, presets } from 'halt';
757
- import { haltMiddleware, createLimiter } from 'halt/express';
758
-
759
- const app = express();
760
-
761
- // Global rate limit
762
- const globalLimiter = new RateLimiter({
763
- store: new InMemoryStore(),
764
- policy: presets.GENEROUS_API,
765
- });
766
- app.use(haltMiddleware({ limiter: globalLimiter }));
767
-
768
- // Endpoint-specific limits
769
- const authLimiter = new RateLimiter({
770
- store: new InMemoryStore(),
771
- policy: presets.AUTH_ENDPOINTS,
772
- });
773
-
774
- app.post('/auth/login', createLimiter(authLimiter), (req, res) => {
775
- // This endpoint has BOTH global AND auth limits
776
- res.json({ token: '...' });
777
- });
778
- ```
779
-
780
- ### Custom Blocked Response
781
-
782
- ```typescript
783
- import { haltMiddleware } from 'halt/express';
784
-
785
- app.use(haltMiddleware({
786
- limiter,
787
- onBlocked: (req, res) => {
788
- res.status(429).json({
789
- error: 'RATE_LIMIT_EXCEEDED',
790
- message: 'Slow down! Try again later.',
791
- timestamp: Date.now(),
792
- });
793
- },
794
- }));
795
- ```
796
-
797
- ---
798
-
799
- ## Testing
800
-
801
- ```typescript
802
- import { describe, it, expect } from 'vitest';
803
- import { RateLimiter, InMemoryStore, Policy, Algorithm } from 'halt';
804
-
805
- describe('Rate Limiting', () => {
806
- it('should block after limit exceeded', () => {
807
- const policy: Policy = {
808
- name: 'test',
809
- limit: 5,
810
- window: 60,
811
- algorithm: Algorithm.TOKEN_BUCKET,
812
- };
813
-
814
- const limiter = new RateLimiter({
815
- store: new InMemoryStore(),
816
- policy,
817
- });
818
-
819
- // Mock request
820
- const request = {
821
- socket: { remoteAddress: '127.0.0.1' },
822
- headers: {},
823
- };
824
-
825
- // First 5 requests should succeed
826
- for (let i = 0; i < 5; i++) {
827
- const decision = limiter.check(request);
828
- expect(decision.allowed).toBe(true);
829
- }
830
-
831
- // 6th request should be blocked
832
- const decision = limiter.check(request);
833
- expect(decision.allowed).toBe(false);
834
- expect(decision.retryAfter).toBeGreaterThan(0);
835
- });
836
- });
837
- ```
838
-
839
- ---
840
-
841
- ## Troubleshooting
842
-
843
- ### Rate limits not working?
844
-
845
- 1. **Check if request is exempted:**
846
- - Health check paths are auto-exempted
847
- - Private IPs are auto-exempted (disable with `exemptPrivateIps: false`)
848
-
849
- 2. **Verify key extraction:**
850
- ```typescript
851
- // Debug key extraction
852
- const key = (limiter as any).extractKey(request);
853
- console.log('Rate limit key:', key);
854
- ```
855
-
856
- 3. **Check storage:**
857
- - InMemoryStore doesn't persist across restarts
858
- - Each process has its own memory store
859
-
860
- ### Headers not appearing?
861
-
862
- Make sure middleware is added correctly and responses are going through the middleware chain.
863
-
864
- ### Different limits for same IP?
865
-
866
- You might be using different policy names. Each policy maintains separate counters:
867
-
868
- ```typescript
869
- // These are SEPARATE limits
870
- const policy1: Policy = { name: 'api_v1', limit: 100, window: 60 };
871
- const policy2: Policy = { name: 'api_v2', limit: 100, window: 60 };
872
- ```
873
-
874
- ---
875
-
876
- ## Performance
877
-
878
- | Algorithm | Throughput | Memory | Accuracy |
879
- |-----------|-----------|--------|----------|
880
- | Token Bucket | ~100k req/s | Low | High |
881
- | Fixed Window | ~120k req/s | Very Low | Medium |
882
- | Sliding Window | ~80k req/s | Medium | Very High |
883
- | Leaky Bucket | ~90k req/s | Low | High |
884
-
885
- *Benchmarks on M1 Mac, in-memory storage*
886
-
887
- All algorithms use O(1) memory per key (except Sliding Window which uses O(precision) per key).
888
-
889
- ---
890
-
891
- ## TypeScript Support
37
+ ## Features
892
38
 
893
- Halt is written in TypeScript and provides full type safety:
39
+ - Algorithms: token bucket, fixed/sliding window, leaky bucket
40
+ - Keys: IP, user, API key, composite, or custom
41
+ - Atomic **Redis** store (Lua, cluster-safe, fail-open) + in-memory dev store
42
+ - **Dynamic limits**: change limits at runtime with `PolicyRegistry` / cached loaders — no restart
43
+ - SaaS: per-plan limits, quotas, weighted endpoints, abuse penalties
44
+ - Observability: `StatsCollector` + OpenTelemetry metrics
45
+ - Adapters: Express, Next.js, **Hono**, **Fastify**, GraphQL (Apollo)
894
46
 
895
- ```typescript
896
- import type { Policy, Decision, RateLimiterOptions } from 'halt';
47
+ ## Runtime support
897
48
 
898
- const policy: Policy = {
899
- name: 'typed',
900
- limit: 100,
901
- window: 60,
902
- };
49
+ Ships dual **ESM + CJS** with types. The core (limiter, algorithms, in-memory store) is
50
+ **edge-safe** — runs on Cloudflare Workers, Deno, Bun, and Vercel Edge. `RedisStore` is Node-only
51
+ (TCP); for edge distributed limits, inject any `RedisClientLike` (e.g. a fetch/REST client).
52
+ Full guidance: **[halt.afroawi.com/docs](https://halt.afroawi.com/docs)**.
903
53
 
904
- const decision: Decision = limiter.check(request);
905
- ```
54
+ ## Links
906
55
 
907
- ---
56
+ - Docs: https://halt.afroawi.com
57
+ - npm: https://www.npmjs.com/package/halt-rate
58
+ - Source & issues: https://github.com/surafel-kindu/halt
908
59
 
909
60
  ## License
910
61
 
911
62
  MIT
912
-
913
- ---
914
-
915
- ## Contributing
916
-
917
- Contributions welcome! Please open an issue or PR on GitHub.
918
-
919
- ---
920
-
921
- ## Roadmap
922
-
923
- ### v0.3 (Current)
924
- - ✅ Token Bucket algorithm
925
- - ✅ Fixed Window algorithm
926
- - ✅ Sliding Window algorithm
927
- - ✅ Leaky Bucket algorithm
928
- - ✅ In-memory storage
929
- - ✅ PostgreSQL storage
930
- - ✅ MongoDB storage
931
- - ✅ DynamoDB storage
932
- - ✅ Memcached storage
933
- - ✅ Quota system
934
- - ✅ Penalty system
935
- - ✅ Telemetry hooks
936
- - ✅ Plan-based presets
937
- - ⏳ Redis storage
938
-
939
- ### v0.4 (Next)
940
- - OpenTelemetry integration
941
- - Distributed global limits
942
- - Idempotent response mode
943
- - Enhanced metrics and dashboards
944
-
945
- ### v1.0 (Future)
946
- - Adaptive limits
947
- - Advanced abuse detection
948
- - Multi-region support
949
- - GraphQL support