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.0Limiter 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.
npm install limiterimport { RateLimiter, TokenBucket } from "limiter";For CommonJS:
const { RateLimiter, TokenBucket } = require("limiter");import { RateLimiter } from "limiter";
// Allow 150 requests per hour (Twitter search limit example)
const limiter = new RateLimiter({ tokensPerInterval: 150, interval: "hour" });
async function sendRequest() {
// Remove a token, waiting if necessary
const remainingRequests = await limiter.removeTokens(1);
console.log(`${remainingRequests} requests remaining`);
// Make your API call here
}
// Synchronous token removal
if (limiter.tryRemoveTokens(1)) {
console.log('Token removed successfully');
} else {
console.log('No tokens available');
}Limiter is built around two core components:
High-level rate limiting with configurable tokens per interval and optional immediate response mode.
class RateLimiter {
/**
* Create a new rate limiter
* @param options Configuration options for the rate limiter
*/
constructor(options: RateLimiterOpts);
/**
* Remove the requested number of tokens. Waits if necessary until tokens are available.
* @param count The number of tokens to remove (must not exceed tokensPerInterval)
* @returns Promise resolving to the number of tokens remaining after removal
* @throws Error if count exceeds tokensPerInterval
*/
removeTokens(count: number): Promise<number>;
/**
* Attempt to remove tokens immediately without waiting
* @param count The number of tokens to remove
* @returns true if tokens were successfully removed, false otherwise
*/
tryRemoveTokens(count: number): boolean;
/**
* Get the current number of tokens remaining in the bucket
* @returns The number of tokens currently available
*/
getTokensRemaining(): number;
/** The underlying token bucket used for rate limiting */
tokenBucket: TokenBucket;
/** Timestamp marking the start of the current interval */
curIntervalStart: number;
/** Number of tokens consumed in the current interval */
tokensThisInterval: number;
/** Whether to fire immediately when rate limiting is in effect */
fireImmediately: boolean;
}
interface RateLimiterOpts {
/** Maximum number of tokens that can be removed per interval */
tokensPerInterval: number;
/** The interval length in milliseconds or as a string (e.g., 'second', 'minute', 'hour', 'day') */
interval: Interval;
/** Whether to return immediately with -1 when rate limiting is active (default: false) */
fireImmediately?: boolean;
}Usage Examples:
import { RateLimiter } from "limiter";
// Basic rate limiting - 10 requests per second
const limiter = new RateLimiter({ tokensPerInterval: 10, interval: "second" });
// Async usage (waits for tokens to become available)
async function makeRequest() {
const remaining = await limiter.removeTokens(1);
console.log(`Request made, ${remaining} tokens remaining`);
}
// Immediate response mode for HTTP 429 handling
const apiLimiter = new RateLimiter({
tokensPerInterval: 150,
interval: "hour",
fireImmediately: true
});
async function handleApiRequest(request, response) {
const remaining = await apiLimiter.removeTokens(1);
if (remaining < 0) {
response.writeHead(429, {'Content-Type': 'text/plain'});
response.end('429 Too Many Requests - rate limited');
} else {
// Process request normally
}
}
// Check remaining tokens without removing any
console.log(`Current tokens: ${limiter.getTokensRemaining()}`);Lower-level hierarchical token bucket for precise rate control with burst capabilities and parent-child relationships.
class TokenBucket {
/**
* Create a new token bucket
* @param options Configuration options for the token bucket
*/
constructor(options: TokenBucketOpts);
/**
* Remove tokens from the bucket, waiting if necessary until enough tokens are available
* @param count The number of tokens to remove (must not exceed bucketSize)
* @returns Promise resolving to the number of tokens remaining after removal
* @throws Error if count exceeds bucketSize
*/
removeTokens(count: number): Promise<number>;
/**
* Attempt to remove tokens immediately without waiting
* @param count The number of tokens to remove
* @returns true if tokens were successfully removed, false otherwise
*/
tryRemoveTokens(count: number): boolean;
/**
* Add any new tokens to the bucket based on time elapsed since last drip
* @returns true if new tokens were added, false otherwise
*/
drip(): boolean;
/** Maximum number of tokens the bucket can hold (burst rate) */
bucketSize: number;
/** Number of tokens added to the bucket per interval */
tokensPerInterval: number;
/** The interval length in milliseconds */
interval: number;
/** Optional parent bucket for hierarchical rate limiting */
parentBucket?: TokenBucket;
/** Current number of tokens in the bucket */
content: number;
/** Timestamp of the last token drip operation */
lastDrip: number;
}
interface TokenBucketOpts {
/** Maximum number of tokens the bucket can hold (burst rate) */
bucketSize: number;
/** Number of tokens to add to the bucket per interval */
tokensPerInterval: number;
/** The interval length in milliseconds or as a string (e.g., 'second', 'minute', 'hour', 'day') */
interval: Interval;
/** Optional parent bucket for hierarchical rate limiting */
parentBucket?: TokenBucket;
}Usage Examples:
import { TokenBucket } from "limiter";
// Byte-level throttling at 50KB/sec sustained, 150KB/sec burst
const BURST_RATE = 1024 * 1024 * 150; // 150KB/sec burst
const FILL_RATE = 1024 * 1024 * 50; // 50KB/sec sustained
const bucket = new TokenBucket({
bucketSize: BURST_RATE,
tokensPerInterval: FILL_RATE,
interval: "second"
});
async function handleData(data) {
await bucket.removeTokens(data.byteLength);
sendData(data);
}
// Hierarchical token buckets
const parentBucket = new TokenBucket({
bucketSize: 1000,
tokensPerInterval: 100,
interval: "second"
});
const childBucket = new TokenBucket({
bucketSize: 100,
tokensPerInterval: 50,
interval: "second",
parentBucket: parentBucket
});
// Removing tokens from child also removes from parent
await childBucket.removeTokens(10);
// Manual token drip
if (bucket.drip()) {
console.log("New tokens were added to the bucket");
}
// Infinite bucket (bucketSize: 0)
const infiniteBucket = new TokenBucket({
bucketSize: 0,
tokensPerInterval: 10,
interval: "second"
});type Interval = number | "second" | "sec" | "minute" | "min" | "hour" | "hr" | "day";The Interval type supports both numeric values (milliseconds) and convenient string shortcuts:
"second" or "sec": 1000ms"minute" or "min": 60,000ms"hour" or "hr": 3,600,000ms"day": 86,400,000msBoth RateLimiter and TokenBucket throw errors in specific scenarios:
removeTokens(count): Throws Error if count exceeds tokensPerIntervalError if interval is an invalid stringremoveTokens(count): Throws Error if count exceeds bucketSizeError if interval is an invalid stringtry {
// Invalid interval string
const limiter = new RateLimiter({
tokensPerInterval: 10,
interval: "invalid" as any
});
} catch (error) {
console.error("Invalid interval:", error.message);
// Output: "Invalid interval invalid"
}
try {
// Requesting more tokens than allowed per interval
const limiter = new RateLimiter({ tokensPerInterval: 10, interval: "second" });
await limiter.removeTokens(15); // More than tokensPerInterval
} catch (error) {
console.error("Too many tokens requested:", error.message);
// Output: "Requested tokens 15 exceeds maximum tokens per interval 10"
}
try {
// Requesting more tokens than bucket size
const bucket = new TokenBucket({
bucketSize: 5,
tokensPerInterval: 10,
interval: "second"
});
await bucket.removeTokens(10); // More than bucketSize
} catch (error) {
console.error("Bucket size exceeded:", error.message);
// Output: "Requested tokens 10 exceeds bucket size 5"
}"second", "sec", "minute", "min", "hour", "hr", "day"removeTokens() callstryRemoveTokens() for non-blocking token removal when immediate response is neededRateLimiter automatically manages interval resets and token bucket refillsNumber.POSITIVE_INFINITY