A generic rate limiter for the web and node.js, useful for API clients, web crawling, or other tasks that need to be throttled
npx @tessl/cli install tessl/npm-limiter@3.0.00
# Limiter
1
2
Limiter provides a generic rate limiter for the web and Node.js. It's useful for API clients, web crawling, or other tasks that need to be throttled. The library implements two complementary classes: `RateLimiter` provides a high-level interface for enforcing rate limits with configurable tokens per interval, while `TokenBucket` offers a lower-level interface with configurable burst and drip rates.
3
4
## Package Information
5
6
- **Package Name**: limiter
7
- **Package Type**: npm
8
- **Language**: TypeScript
9
- **Installation**: `npm install limiter`
10
11
## Core Imports
12
13
```typescript
14
import { RateLimiter, TokenBucket } from "limiter";
15
```
16
17
For CommonJS:
18
19
```javascript
20
const { RateLimiter, TokenBucket } = require("limiter");
21
```
22
23
## Basic Usage
24
25
```typescript
26
import { RateLimiter } from "limiter";
27
28
// Allow 150 requests per hour (Twitter search limit example)
29
const limiter = new RateLimiter({ tokensPerInterval: 150, interval: "hour" });
30
31
async function sendRequest() {
32
// Remove a token, waiting if necessary
33
const remainingRequests = await limiter.removeTokens(1);
34
console.log(`${remainingRequests} requests remaining`);
35
// Make your API call here
36
}
37
38
// Synchronous token removal
39
if (limiter.tryRemoveTokens(1)) {
40
console.log('Token removed successfully');
41
} else {
42
console.log('No tokens available');
43
}
44
```
45
46
## Architecture
47
48
Limiter is built around two core components:
49
50
- **RateLimiter**: High-level rate limiter that combines a token bucket with interval-based restrictions to comply with common API limits like "150 requests per hour maximum"
51
- **TokenBucket**: Lower-level hierarchical token bucket implementation providing configurable burst rate and drip rate with optional parent-child relationships
52
- **Clock Utilities**: Internal high-resolution timing functions for precise rate limiting calculations
53
54
## Capabilities
55
56
### Rate Limiting
57
58
High-level rate limiting with configurable tokens per interval and optional immediate response mode.
59
60
```typescript { .api }
61
class RateLimiter {
62
/**
63
* Create a new rate limiter
64
* @param options Configuration options for the rate limiter
65
*/
66
constructor(options: RateLimiterOpts);
67
68
/**
69
* Remove the requested number of tokens. Waits if necessary until tokens are available.
70
* @param count The number of tokens to remove (must not exceed tokensPerInterval)
71
* @returns Promise resolving to the number of tokens remaining after removal
72
* @throws Error if count exceeds tokensPerInterval
73
*/
74
removeTokens(count: number): Promise<number>;
75
76
/**
77
* Attempt to remove tokens immediately without waiting
78
* @param count The number of tokens to remove
79
* @returns true if tokens were successfully removed, false otherwise
80
*/
81
tryRemoveTokens(count: number): boolean;
82
83
/**
84
* Get the current number of tokens remaining in the bucket
85
* @returns The number of tokens currently available
86
*/
87
getTokensRemaining(): number;
88
89
/** The underlying token bucket used for rate limiting */
90
tokenBucket: TokenBucket;
91
/** Timestamp marking the start of the current interval */
92
curIntervalStart: number;
93
/** Number of tokens consumed in the current interval */
94
tokensThisInterval: number;
95
/** Whether to fire immediately when rate limiting is in effect */
96
fireImmediately: boolean;
97
}
98
99
interface RateLimiterOpts {
100
/** Maximum number of tokens that can be removed per interval */
101
tokensPerInterval: number;
102
/** The interval length in milliseconds or as a string (e.g., 'second', 'minute', 'hour', 'day') */
103
interval: Interval;
104
/** Whether to return immediately with -1 when rate limiting is active (default: false) */
105
fireImmediately?: boolean;
106
}
107
```
108
109
**Usage Examples:**
110
111
```typescript
112
import { RateLimiter } from "limiter";
113
114
// Basic rate limiting - 10 requests per second
115
const limiter = new RateLimiter({ tokensPerInterval: 10, interval: "second" });
116
117
// Async usage (waits for tokens to become available)
118
async function makeRequest() {
119
const remaining = await limiter.removeTokens(1);
120
console.log(`Request made, ${remaining} tokens remaining`);
121
}
122
123
// Immediate response mode for HTTP 429 handling
124
const apiLimiter = new RateLimiter({
125
tokensPerInterval: 150,
126
interval: "hour",
127
fireImmediately: true
128
});
129
130
async function handleApiRequest(request, response) {
131
const remaining = await apiLimiter.removeTokens(1);
132
if (remaining < 0) {
133
response.writeHead(429, {'Content-Type': 'text/plain'});
134
response.end('429 Too Many Requests - rate limited');
135
} else {
136
// Process request normally
137
}
138
}
139
140
// Check remaining tokens without removing any
141
console.log(`Current tokens: ${limiter.getTokensRemaining()}`);
142
```
143
144
### Token Bucket
145
146
Lower-level hierarchical token bucket for precise rate control with burst capabilities and parent-child relationships.
147
148
```typescript { .api }
149
class TokenBucket {
150
/**
151
* Create a new token bucket
152
* @param options Configuration options for the token bucket
153
*/
154
constructor(options: TokenBucketOpts);
155
156
/**
157
* Remove tokens from the bucket, waiting if necessary until enough tokens are available
158
* @param count The number of tokens to remove (must not exceed bucketSize)
159
* @returns Promise resolving to the number of tokens remaining after removal
160
* @throws Error if count exceeds bucketSize
161
*/
162
removeTokens(count: number): Promise<number>;
163
164
/**
165
* Attempt to remove tokens immediately without waiting
166
* @param count The number of tokens to remove
167
* @returns true if tokens were successfully removed, false otherwise
168
*/
169
tryRemoveTokens(count: number): boolean;
170
171
/**
172
* Add any new tokens to the bucket based on time elapsed since last drip
173
* @returns true if new tokens were added, false otherwise
174
*/
175
drip(): boolean;
176
177
/** Maximum number of tokens the bucket can hold (burst rate) */
178
bucketSize: number;
179
/** Number of tokens added to the bucket per interval */
180
tokensPerInterval: number;
181
/** The interval length in milliseconds */
182
interval: number;
183
/** Optional parent bucket for hierarchical rate limiting */
184
parentBucket?: TokenBucket;
185
/** Current number of tokens in the bucket */
186
content: number;
187
/** Timestamp of the last token drip operation */
188
lastDrip: number;
189
}
190
191
interface TokenBucketOpts {
192
/** Maximum number of tokens the bucket can hold (burst rate) */
193
bucketSize: number;
194
/** Number of tokens to add to the bucket per interval */
195
tokensPerInterval: number;
196
/** The interval length in milliseconds or as a string (e.g., 'second', 'minute', 'hour', 'day') */
197
interval: Interval;
198
/** Optional parent bucket for hierarchical rate limiting */
199
parentBucket?: TokenBucket;
200
}
201
```
202
203
**Usage Examples:**
204
205
```typescript
206
import { TokenBucket } from "limiter";
207
208
// Byte-level throttling at 50KB/sec sustained, 150KB/sec burst
209
const BURST_RATE = 1024 * 1024 * 150; // 150KB/sec burst
210
const FILL_RATE = 1024 * 1024 * 50; // 50KB/sec sustained
211
212
const bucket = new TokenBucket({
213
bucketSize: BURST_RATE,
214
tokensPerInterval: FILL_RATE,
215
interval: "second"
216
});
217
218
async function handleData(data) {
219
await bucket.removeTokens(data.byteLength);
220
sendData(data);
221
}
222
223
// Hierarchical token buckets
224
const parentBucket = new TokenBucket({
225
bucketSize: 1000,
226
tokensPerInterval: 100,
227
interval: "second"
228
});
229
230
const childBucket = new TokenBucket({
231
bucketSize: 100,
232
tokensPerInterval: 50,
233
interval: "second",
234
parentBucket: parentBucket
235
});
236
237
// Removing tokens from child also removes from parent
238
await childBucket.removeTokens(10);
239
240
// Manual token drip
241
if (bucket.drip()) {
242
console.log("New tokens were added to the bucket");
243
}
244
245
// Infinite bucket (bucketSize: 0)
246
const infiniteBucket = new TokenBucket({
247
bucketSize: 0,
248
tokensPerInterval: 10,
249
interval: "second"
250
});
251
```
252
253
## Types
254
255
```typescript { .api }
256
type Interval = number | "second" | "sec" | "minute" | "min" | "hour" | "hr" | "day";
257
```
258
259
The `Interval` type supports both numeric values (milliseconds) and convenient string shortcuts:
260
- `"second"` or `"sec"`: 1000ms
261
- `"minute"` or `"min"`: 60,000ms
262
- `"hour"` or `"hr"`: 3,600,000ms
263
- `"day"`: 86,400,000ms
264
265
## Error Handling
266
267
Both `RateLimiter` and `TokenBucket` throw errors in specific scenarios:
268
269
### RateLimiter Errors
270
- **`removeTokens(count)`**: Throws `Error` if `count` exceeds `tokensPerInterval`
271
- **Constructor**: Throws `Error` if `interval` is an invalid string
272
273
### TokenBucket Errors
274
- **`removeTokens(count)`**: Throws `Error` if `count` exceeds `bucketSize`
275
- **Constructor**: Throws `Error` if `interval` is an invalid string
276
277
### Error Examples
278
279
```typescript
280
try {
281
// Invalid interval string
282
const limiter = new RateLimiter({
283
tokensPerInterval: 10,
284
interval: "invalid" as any
285
});
286
} catch (error) {
287
console.error("Invalid interval:", error.message);
288
// Output: "Invalid interval invalid"
289
}
290
291
try {
292
// Requesting more tokens than allowed per interval
293
const limiter = new RateLimiter({ tokensPerInterval: 10, interval: "second" });
294
await limiter.removeTokens(15); // More than tokensPerInterval
295
} catch (error) {
296
console.error("Too many tokens requested:", error.message);
297
// Output: "Requested tokens 15 exceeds maximum tokens per interval 10"
298
}
299
300
try {
301
// Requesting more tokens than bucket size
302
const bucket = new TokenBucket({
303
bucketSize: 5,
304
tokensPerInterval: 10,
305
interval: "second"
306
});
307
await bucket.removeTokens(10); // More than bucketSize
308
} catch (error) {
309
console.error("Bucket size exceeded:", error.message);
310
// Output: "Requested tokens 10 exceeds bucket size 5"
311
}
312
```
313
314
### Valid Interval Values
315
- **Numeric**: Any positive number (milliseconds)
316
- **String shortcuts**: `"second"`, `"sec"`, `"minute"`, `"min"`, `"hour"`, `"hr"`, `"day"`
317
318
## Additional Notes
319
320
- Both classes should be used with message queues or serialization to prevent race conditions with simultaneous `removeTokens()` calls
321
- Race conditions can lead to out-of-order messages or apparent "lost" messages under heavy load
322
- Use `tryRemoveTokens()` for non-blocking token removal when immediate response is needed
323
- The `RateLimiter` automatically manages interval resets and token bucket refills
324
- Token buckets support hierarchical relationships where child buckets also consume tokens from parent buckets
325
- Infinite buckets (bucketSize: 0) always allow token removal and return `Number.POSITIVE_INFINITY`