leakykit
v0.1.0
Published
Zero-dependency TypeScript leaky bucket rate limiter. Smooth traffic to a constant rate with optional burst capacity. Port of Python ratelimiter / Go golang.org/x/time/rate leaky-bucket mode.
Maintainers
Readme
leakykit
Zero-dependency TypeScript leaky bucket rate limiter. Smooths traffic to a constant output rate — no bursts. Port of Python ratelimiter / Go golang.org/x/time/rate leaky-bucket mode.
Install
npm install leakykitLeaky bucket vs. token bucket
Both limit request rates, but they differ in one key way:
| | Token bucket | Leaky bucket |
|---|---|---|
| Burst traffic | Allowed (up to capacity) | Not allowed — smoothed to constant rate |
| Use case | APIs that permit short bursts | Outgoing webhooks, SMS, strict per-second limits |
| npm alternatives | limiter, bottleneck | leaky-bucket (abandoned 2021) |
Usage
import { LeakyBucket } from "leakykit";
// Allow 10 requests per second (one every 100ms)
const bucket = new LeakyBucket({ capacity: 10, interval: 1000 });
for (const request of requests) {
await bucket.throttle(); // waits if needed, then resolves
await sendRequest(request);
}With AbortSignal
const ctrl = new AbortController();
try {
await bucket.throttle(1, { signal: ctrl.signal });
} catch (e) {
if (e instanceof DOMException && e.name === "AbortError") {
console.log("Request was cancelled");
}
}
// Cancel all pending requests:
ctrl.abort();Variable cost per request
// Bulk operations cost more tokens
const bucket = new LeakyBucket({ capacity: 100, interval: 1000 }); // 100 units/s
await bucket.throttle(1); // single item — 1 unit
await bucket.throttle(10); // bulk batch — 10 units
await bucket.throttle(50); // large batch — 50 unitsRate-limit API calls to an external service
import { LeakyBucket } from "leakykit";
// Stripe allows 100 req/s in live mode
const stripe = new LeakyBucket({ capacity: 100, interval: 1000 });
async function chargeCustomers(customers: Customer[]) {
return Promise.all(customers.map(async customer => {
await stripe.throttle();
return fetch(`/api/charge/${customer.id}`, { method: "POST" });
}));
}Graceful shutdown
const bucket = new LeakyBucket({ capacity: 10, interval: 1000 });
// On shutdown, reject all waiting requests
process.on("SIGTERM", () => {
bucket.abort(new Error("Server shutting down"));
});API
new LeakyBucket(options)
interface LeakyBucketOptions {
capacity: number; // max requests per interval
interval: number; // time window in milliseconds
}throttle(cost?, opts?): Promise<void>
Wait for cost tokens to become available, then resolve. Calls are queued in FIFO order.
cost: tokens to consume (default:1, must be1..capacity)opts.signal: anAbortSignalto cancel this specific wait
Throws RangeError if cost <= 0 or cost > capacity.
abort(reason?): void
Reject all currently-pending throttle() calls with reason (default: DOMException("AbortError")). Does not affect already-resolved calls.
drain(): void
Alias for abort(new Error("LeakyBucket drained")).
availableTokens: number
Current available capacity (accounting for time elapsed since last request).
capacity: number / interval: number
Read-only configuration values.
How it works
The bucket starts full (capacity tokens). Each throttle() call:
- Checks how many tokens have leaked back since the last call (
elapsed * capacity / interval). - If enough tokens are available, consumes them and resolves immediately.
- Otherwise, schedules a
setTimeoutfor when enough tokens will have leaked, then resolves.
This is the same algorithm used by nginx's limit_req_zone, Stripe's API rate limiter, and Go's golang.org/x/time/rate in leaky-bucket mode.
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
