exchequer
v0.1.0
Published
Minimal async token-bucket rate limiter — refill R/sec, burst B, await for capacity. Zero deps, isomorphic, injectable clock.
Maintainers
Readme
exchequer
A minimal async token-bucket rate limiter for JavaScript and TypeScript.
Features
- Simple API — Just
take()andtryTake()methods - FIFO ordering — Waiters are served in first-in-first-out order
- Fractional tokens — Precise rate limiting with fractional token precision
- Zero dependencies — No runtime dependencies, minimal bundle size
- Runtime agnostic — Works in Node.js, browsers, and edge runtimes
- Clock injection — Deterministic testing with injectable time source
- Type-safe — Full TypeScript support with strict mode
Installation
npm install exchequer
# or
pnpm add exchequer
# or
yarn add exchequerQuick Start
import exchequer from "exchequer";
// Create a bucket: 10 tokens per second, burst capacity of 20
const bucket = exchequer({ rate: 10, burst: 20 });
// Try to take a token without blocking
if (bucket.tryTake(1)) {
console.log("Token acquired!");
}
// Or wait asynchronously until a token is available
await bucket.take(1);
console.log("Token acquired!");How It Works
Token buckets continuously refill at rate tokens per second, up to a maximum of burst tokens. When tokens are insufficient, take() waits asynchronously in FIFO order.
- Burst capacity — Maximum tokens that can be accumulated
- Rate — Tokens added per second (can be fractional)
- FIFO queue — Waiters are served in order of arrival
- Fractional precision — Token calculations maintain fractional precision
API
exchequer(options)
Creates a new token bucket rate limiter.
Options
interface ExchequerOptions {
rate: number; // Tokens per second (must be > 0)
burst?: number; // Maximum bucket capacity (defaults to rate)
now?: () => number; // Current time in ms (defaults to Date.now)
setTimer?: (callback: () => void, ms: number) => { clear: () => void };
}rate— Number of tokens added per second. Must be greater than 0.burst— Maximum bucket capacity. Defaults torateif not specified.now— Function that returns current time in milliseconds since epoch. Used for deterministic testing.setTimer— Function to schedule a callback after a delay. Used for testing and scheduler injection.
Returns
interface Exchequer {
take(count?: number): Promise<void>;
tryTake(count?: number): boolean;
readonly tokens: number;
reset(): void;
}bucket.take(count)
Asynchronously wait until count tokens are available.
- count — Number of tokens to wait for (defaults to 1)
- Returns — Promise that resolves when tokens are available
- Throws —
RangeErrorif count ≤ 0 or exceeds burst capacity
// Wait for 1 token (default)
await bucket.take();
// Wait for 5 tokens
await bucket.take(5);bucket.tryTake(count)
Try to take tokens without blocking.
- count — Number of tokens to take (defaults to 1)
- Returns —
trueif tokens were taken,falseif insufficient tokens
// Try to take 1 token
if (bucket.tryTake()) {
console.log("Token acquired!");
} else {
console.log("Not enough tokens.");
}
// Try to take 5 tokens
if (bucket.tryTake(5)) {
console.log("5 tokens acquired!");
} else {
console.log("Not enough tokens for 5.");
}bucket.tokens
Current number of available tokens (can be fractional). Lazily calculated based on elapsed time since last operation.
console.log(`Current tokens: ${bucket.tokens}`);bucket.reset()
Reset the bucket to full burst capacity. Any queued waiters will be served immediately if enough tokens are available.
bucket.reset();
console.log(`Tokens after reset: ${bucket.tokens}`); // Will show burst capacityUsage Examples
API Rate Limiting
import exchequer from "exchequer";
// Limit API calls to 10 per second with burst of 20
const apiLimiter = exchequer({ rate: 10, burst: 20 });
async function callAPI(url) {
// Wait for token before making API call
await apiLimiter.take();
return fetch(url);
}Processing Batches
import exchequer from "exchequer";
// Process items at a steady rate of 100 per second
const processor = exchequer({ rate: 100, burst: 200 });
async function processBatch(items) {
for (const item of items) {
await processor.take();
await processItem(item);
}
}Non-blocking Token Check
import exchequer from "exchequer";
const bucket = exchequer({ rate: 5, burst: 10 });
function handleRequest() {
if (bucket.tryTake()) {
// Process request immediately
processRequest();
} else {
// Rate limited - reject request
sendError("Too many requests");
}
}Deterministic Testing
import exchequer from "exchequer";
// Inject clock for deterministic testing
let time = 0;
const bucket = exchequer({
rate: 10,
burst: 5,
now: () => time,
setTimer: (cb, ms) => {
const timeout = setTimeout(() => {
time += ms;
cb();
}, 10); // Short delay for testing
return { clear: () => clearTimeout(timeout) };
}
});
// Now you can control time in tests
time = 0;
console.log(bucket.tokens); // 5 (burst)
time = 500; // Advance 500ms
console.log(bucket.tokens); // 10 (5 burst + 5 from rate)What exchequer does NOT do
This package has clear boundaries and intentionally does NOT provide:
- Distributed coordination — Single-process, in-memory only
- Priority queues — Strict FIFO ordering only, no priorities
- Resource pooling — Not a reservoir scheduler for resource management
- Cluster-aware limiting — No distributed rate limiting across processes
- HTTP 429 handling — Use the
forbearpackage for response-based rate limiting
Fractional Token Precision
The bucket maintains fractional token precision for accurate rate limiting:
const bucket = exchequer({ rate: 10, burst: 10 });
// Drain all tokens
bucket.tryTake(10);
console.log(bucket.tokens); // 0
// Advance 333ms (should get 3.33 tokens)
// In real usage, time advances naturally
// For testing, inject time controlError Handling
The package validates inputs and throws RangeError for invalid parameters:
const bucket = exchequer({ rate: 10, burst: 5 });
// These will throw RangeError:
await bucket.take(0); // count must be > 0
await bucket.take(-1); // count must be > 0
await bucket.take(6); // count (6) cannot exceed burst (5)Type Safety
Full TypeScript support with comprehensive type definitions:
import exchequer from "exchequer";
const bucket: Exchequer = exchequer({ rate: 10, burst: 20 });
// Full type safety and autocomplete
await bucket.take(1);
const success = bucket.tryTake(1);
const currentTokens = bucket.tokens;
bucket.reset();License
MIT
See Also
forbear— Read server rate-limit instructions from HTTP responses- Other packages in the npmdevkit monorepo
