ratebucket
v0.1.1
Published
Token-bucket rate limiter with fair FIFO waiting, weighted costs, and AbortSignal support. Zero dependencies.
Downloads
258
Maintainers
Readme
ratebucket
Token-bucket rate limiter with fair FIFO waiting, weighted costs, and
AbortSignalsupport. Zero dependencies.
The best way to handle a 429 Too Many Requests is to never trigger it.
ratebucket paces your outbound calls to stay under an API's limit — N requests
per second, with a configurable burst — using a classic token bucket. Waiters are
served fairly (FIFO), costs can be weighted, and any wait is cancellable.
import { RateLimiter } from "ratebucket";
const limiter = new RateLimiter({ rate: 5, interval: 1000 }); // 5 requests/second
await Promise.all(
jobs.map((job) => limiter.schedule(() => callApi(job))),
); // never more than ~5 calls hit the API per secondWhy ratebucket?
- Token bucket done right. Continuous refill, a burst capacity, and fair FIFO ordering so no caller starves.
- Weighted costs. Charge more than one token per call to respect token-based budgets (e.g. an LLM's tokens-per-minute limit), not just request counts.
- Cancellable & non-blocking options.
acquirewaits,tryAcquirenever blocks, and anAbortSignalcleanly cancels a pending wait. - Deterministic & testable. Inject a
clockfor exact, time-free tests. - Zero dependencies, ESM + CJS + types.
Install
npm install ratebucket
# or: pnpm add ratebucket / yarn add ratebucket / bun add ratebucketAPI
new RateLimiter(options)
| Option | Type | Default | Description |
| ---------- | -------------- | ---------- | --------------------------------------------- |
| rate | number | (required) | Tokens replenished per interval. |
| interval | number (ms) | 1000 | Refill window — rate per this many ms. |
| burst | number | rate | Bucket capacity (max instantaneous burst). |
| clock | () => number | Date.now | Injectable clock for tests. |
acquire(cost?, { signal? }) → Promise<void>
Wait until cost tokens (default 1) are available, then consume them. Rejects
if cost exceeds capacity, or if signal aborts while waiting.
tryAcquire(cost?) → boolean
Consume cost tokens immediately, or return false without waiting (also false
if others are already queued, to preserve fairness).
schedule(fn, cost?, { signal? }) → Promise<T>
acquire(cost) then run fn and return its result.
// Respect an LLM tokens-per-minute budget by charging estimated tokens:
const tpm = new RateLimiter({ rate: 90_000, interval: 60_000, burst: 90_000 });
const reply = await tpm.schedule(() => llm.complete(prompt), estimatedTokens);Introspection
limiter.available; // tokens available right now (refilled to "now")
limiter.pending; // number of callers currently waitingPairs well with
retryfn— if you still hit a 429, retry it (honoringRetry-After).runpool— cap concurrency;ratebucketcaps rate.
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 © Tung Tran
