@billdaddy/leakybucket
v0.1.0
Published
Zero-dependency leaky-bucket rate limiter for Node.js and browsers. Enforces a constant throughput rate — no bursts. TypeScript, ESM+CJS, AbortSignal.
Maintainers
Readme
leakybucket
Zero-dependency leaky-bucket rate limiter for Node.js and browsers. Enforces a constant throughput rate — no bursts. TypeScript, ESM + CJS, AbortSignal.
npm install @billdaddy/leakybucketWhy leaky-bucket?
Unlike a token-bucket (which allows short bursts), a leaky-bucket enforces a uniform spacing of 1000/rate ms between operations. This is what you want when calling external APIs with strict rate limits, throttling database writes, or shaping egress traffic.
| | Token-bucket | Leaky-bucket | |---|---|---| | Burst allowed | ✅ yes | ❌ no | | Constant spacing | ❌ no | ✅ yes | | API rate limit compliance | risky | safe |
Prior art on npm: ts-leaky-bucket was abandoned June 2020 (22 downloads/week), linaGirl/leaky-bucket last commit October 2021. leakybucket is the maintained, zero-dep TypeScript replacement.
Inspired by Go's uber-go/ratelimit.
Quick start
import { LeakyBucket } from "@billdaddy/leakybucket";
const bucket = new LeakyBucket({ rate: 10 }); // 10 ops/sec → 1 op every 100ms
async function callApi(url: string) {
await bucket.take(); // waits for next slot, then proceeds
return fetch(url);
}
// 50 concurrent calls — all proceed in order, spaced 100ms apart
await Promise.all(urls.map(callApi));API
new LeakyBucket(options)
interface LeakyBucketOptions {
rate: number; // operations per second (required, must be > 0)
maxQueue?: number; // max pending requests (default: Infinity)
}const bucket = new LeakyBucket({ rate: 5 }); // 5/sec
const bucket = new LeakyBucket({ rate: 100 }); // 100/sec = 10ms interval
const bucket = new LeakyBucket({ rate: 10, maxQueue: 50 }); // bounded queuebucket.take(signal?): Promise<void>
Acquire a slot. Resolves when it is safe to proceed.
- If no backlog: resolves immediately.
- If backlogged: waits in FIFO order until the next slot opens.
- If
signalis already aborted: rejects immediately. - If aborted while waiting: rejects and removes itself from the queue.
- If queue is full (
maxQueue): rejects withLeakyBucketFullError.
await bucket.take(); // simple usage
await bucket.take(abortController.signal); // cancellablebucket.wrap(fn): limitedFn
Wraps an async function so each call automatically acquires a slot first.
const limitedFetch = bucket.wrap(fetch);
const response = await limitedFetch(url); // rate-limitedbucket.drain()
Reset the internal clock so the next take() proceeds immediately. Useful after a pause or when you want to flush the "debt" without waiting.
Properties
| Property | Description |
|----------|-------------|
| bucket.rate | Configured ops/sec |
| bucket.interval | Ms between ops (1000 / rate) |
| bucket.queueSize | Number of calls currently waiting |
| bucket.waitTime | Ms until next slot is available |
leakyBucket(options) factory
Convenience function for the new LeakyBucket(...) constructor.
import { leakyBucket } from "@billdaddy/leakybucket";
const bucket = leakyBucket({ rate: 10 });LeakyBucketFullError
Thrown when maxQueue is set and the queue is at capacity.
import { LeakyBucketFullError } from "@billdaddy/leakybucket";
try {
await bucket.take();
} catch (e) {
if (e instanceof LeakyBucketFullError) {
console.error("Too many pending requests");
}
}Examples
API rate limiting with AbortSignal
import { LeakyBucket } from "@billdaddy/leakybucket";
const bucket = new LeakyBucket({ rate: 10, maxQueue: 100 });
const controller = new AbortController();
async function fetchWithRateLimit(url: string) {
await bucket.take(controller.signal);
return fetch(url, { signal: controller.signal });
}
// Cancel all pending requests
controller.abort();Wrapping a function
import { LeakyBucket } from "@billdaddy/leakybucket";
const bucket = new LeakyBucket({ rate: 5 });
const limitedSendEmail = bucket.wrap(sendEmail);
// 100 emails sent at most 5/second, in order
for (const email of emails) {
await limitedSendEmail(email);
}Monitoring queue depth
import { LeakyBucket } from "@billdaddy/leakybucket";
const bucket = new LeakyBucket({ rate: 10 });
setInterval(() => {
console.log(`Queue: ${bucket.queueSize}, Wait: ${bucket.waitTime}ms`);
}, 1000);Comparison
| Package | Downloads/week | Last release | TypeScript | Zero-dep | |---------|---------------|--------------|------------|----------| | leakybucket | — | 2024 | ✅ | ✅ | | ts-leaky-bucket | ~22 | 2020 (abandoned) | ❌ | ✅ | | leaky-bucket | ~800 | 2021 (abandoned) | ❌ | ❌ | | limiter | ~65k | 2023 | partial | ✅ (sliding window, not leaky) |
Contributors ✨
This project follows the all-contributors specification. Contributions of any kind are welcome — code, docs, bug reports, ideas, reviews! See the emoji key for how each contribution is recognized, and open a PR or issue to get involved.
Thanks goes to these wonderful people:
License
MIT
