{"_id":"@billdaddy/leakybucket","name":"@billdaddy/leakybucket","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@billdaddy/leakybucket","version":"0.1.0","description":"Zero-dependency leaky-bucket rate limiter for Node.js and browsers. Enforces a constant throughput rate — no bursts. TypeScript, ESM+CJS, AbortSignal.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"}},"scripts":{"build":"tsup","typecheck":"tsc --noEmit","test":"node --experimental-vm-modules node_modules/.bin/jest --forceExit","prepublishOnly":"npm run typecheck && npm test && npm run build"},"keywords":["rate-limiter","leaky-bucket","rate-limit","throttle","concurrency","typescript","zero-dependencies"],"author":{"name":"trananhtung"},"license":"MIT","repository":{"type":"git","url":"git+https://github.com/trananhtung/leakybucket.git"},"devDependencies":{"@types/jest":"^30.0.0","jest":"^30.4.2","ts-jest":"^29.4.11","tsup":"^8.5.1","typescript":"^6.0.3"},"_id":"@billdaddy/leakybucket@0.1.0","gitHead":"06bdd72ea36410cf2d8e2724d9c1924c1d3914ca","bugs":{"url":"https://github.com/trananhtung/leakybucket/issues"},"homepage":"https://github.com/trananhtung/leakybucket#readme","_nodeVersion":"20.18.2","_npmVersion":"11.5.2","dist":{"integrity":"sha512-R2hmARGQRfEUreTb6+AbYiY6DnWKuZOvD+tuBt/xx5OAqPZXhDa6Y1KNJ6FLJA3zu0EeZdO70fBzloi1Th1Ksw==","shasum":"a616c8c8370b46fa1459553951ef305cfa9bebe3","tarball":"https://registry.npmjs.org/@billdaddy/leakybucket/-/leakybucket-0.1.0.tgz","fileCount":9,"unpackedSize":38983,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIHPm0B4h7LxEvMoEDJB6OSZFB2cNwyDWGy14EoQjij+bAiB6UC0L1LyR84VhcXxPuYx1cQG8QOQ+OG/y0PM0AkCH2Q=="}]},"_npmUser":{"name":"billdaddy","email":"tunganhtran94@gmail.com"},"directories":{},"maintainers":[{"name":"billdaddy","email":"tunganhtran94@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/leakybucket_0.1.0_1782279386161_0.4643487272100213"},"_hasShrinkwrap":false}},"time":{"created":"2026-06-24T05:36:25.979Z","0.1.0":"2026-06-24T05:36:26.297Z","modified":"2026-06-24T05:36:26.500Z"},"maintainers":[{"name":"billdaddy","email":"tunganhtran94@gmail.com"}],"description":"Zero-dependency leaky-bucket rate limiter for Node.js and browsers. Enforces a constant throughput rate — no bursts. TypeScript, ESM+CJS, AbortSignal.","homepage":"https://github.com/trananhtung/leakybucket#readme","keywords":["rate-limiter","leaky-bucket","rate-limit","throttle","concurrency","typescript","zero-dependencies"],"repository":{"type":"git","url":"git+https://github.com/trananhtung/leakybucket.git"},"author":{"name":"trananhtung"},"bugs":{"url":"https://github.com/trananhtung/leakybucket/issues"},"license":"MIT","readme":"# leakybucket\n\n[![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-)\n\n[![npm](https://img.shields.io/npm/v/@billdaddy/leakybucket)](https://www.npmjs.com/package/@billdaddy/leakybucket)\n[![CI](https://github.com/trananhtung/leakybucket/actions/workflows/ci.yml/badge.svg)](https://github.com/trananhtung/leakybucket/actions)\n[![license](https://img.shields.io/npm/l/@billdaddy/leakybucket)](LICENSE)\n\nZero-dependency leaky-bucket rate limiter for Node.js and browsers. Enforces a **constant throughput rate** — no bursts. TypeScript, ESM + CJS, AbortSignal.\n\n```bash\nnpm install @billdaddy/leakybucket\n```\n\n## Why leaky-bucket?\n\nUnlike 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.\n\n| | Token-bucket | Leaky-bucket |\n|---|---|---|\n| Burst allowed | ✅ yes | ❌ no |\n| Constant spacing | ❌ no | ✅ yes |\n| API rate limit compliance | risky | safe |\n\n**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.\n\nInspired by Go's [`uber-go/ratelimit`](https://github.com/uber-go/ratelimit).\n\n## Quick start\n\n```ts\nimport { LeakyBucket } from \"@billdaddy/leakybucket\";\n\nconst bucket = new LeakyBucket({ rate: 10 }); // 10 ops/sec → 1 op every 100ms\n\nasync function callApi(url: string) {\n  await bucket.take(); // waits for next slot, then proceeds\n  return fetch(url);\n}\n\n// 50 concurrent calls — all proceed in order, spaced 100ms apart\nawait Promise.all(urls.map(callApi));\n```\n\n## API\n\n### `new LeakyBucket(options)`\n\n```ts\ninterface LeakyBucketOptions {\n  rate: number;       // operations per second (required, must be > 0)\n  maxQueue?: number;  // max pending requests (default: Infinity)\n}\n```\n\n```ts\nconst bucket = new LeakyBucket({ rate: 5 });        // 5/sec\nconst bucket = new LeakyBucket({ rate: 100 });       // 100/sec = 10ms interval\nconst bucket = new LeakyBucket({ rate: 10, maxQueue: 50 }); // bounded queue\n```\n\n### `bucket.take(signal?): Promise<void>`\n\nAcquire a slot. Resolves when it is safe to proceed.\n\n- If no backlog: resolves immediately.\n- If backlogged: waits in FIFO order until the next slot opens.\n- If `signal` is already aborted: rejects immediately.\n- If aborted while waiting: rejects and removes itself from the queue.\n- If queue is full (`maxQueue`): rejects with `LeakyBucketFullError`.\n\n```ts\nawait bucket.take();                   // simple usage\nawait bucket.take(abortController.signal); // cancellable\n```\n\n### `bucket.wrap(fn): limitedFn`\n\nWraps an async function so each call automatically acquires a slot first.\n\n```ts\nconst limitedFetch = bucket.wrap(fetch);\nconst response = await limitedFetch(url); // rate-limited\n```\n\n### `bucket.drain()`\n\nReset the internal clock so the next `take()` proceeds immediately. Useful after a pause or when you want to flush the \"debt\" without waiting.\n\n### Properties\n\n| Property | Description |\n|----------|-------------|\n| `bucket.rate` | Configured ops/sec |\n| `bucket.interval` | Ms between ops (`1000 / rate`) |\n| `bucket.queueSize` | Number of calls currently waiting |\n| `bucket.waitTime` | Ms until next slot is available |\n\n### `leakyBucket(options)` factory\n\nConvenience function for the `new LeakyBucket(...)` constructor.\n\n```ts\nimport { leakyBucket } from \"@billdaddy/leakybucket\";\nconst bucket = leakyBucket({ rate: 10 });\n```\n\n### `LeakyBucketFullError`\n\nThrown when `maxQueue` is set and the queue is at capacity.\n\n```ts\nimport { LeakyBucketFullError } from \"@billdaddy/leakybucket\";\n\ntry {\n  await bucket.take();\n} catch (e) {\n  if (e instanceof LeakyBucketFullError) {\n    console.error(\"Too many pending requests\");\n  }\n}\n```\n\n## Examples\n\n### API rate limiting with AbortSignal\n\n```ts\nimport { LeakyBucket } from \"@billdaddy/leakybucket\";\n\nconst bucket = new LeakyBucket({ rate: 10, maxQueue: 100 });\nconst controller = new AbortController();\n\nasync function fetchWithRateLimit(url: string) {\n  await bucket.take(controller.signal);\n  return fetch(url, { signal: controller.signal });\n}\n\n// Cancel all pending requests\ncontroller.abort();\n```\n\n### Wrapping a function\n\n```ts\nimport { LeakyBucket } from \"@billdaddy/leakybucket\";\n\nconst bucket = new LeakyBucket({ rate: 5 });\nconst limitedSendEmail = bucket.wrap(sendEmail);\n\n// 100 emails sent at most 5/second, in order\nfor (const email of emails) {\n  await limitedSendEmail(email);\n}\n```\n\n### Monitoring queue depth\n\n```ts\nimport { LeakyBucket } from \"@billdaddy/leakybucket\";\n\nconst bucket = new LeakyBucket({ rate: 10 });\n\nsetInterval(() => {\n  console.log(`Queue: ${bucket.queueSize}, Wait: ${bucket.waitTime}ms`);\n}, 1000);\n```\n\n## Comparison\n\n| Package | Downloads/week | Last release | TypeScript | Zero-dep |\n|---------|---------------|--------------|------------|----------|\n| **leakybucket** | — | 2024 | ✅ | ✅ |\n| ts-leaky-bucket | ~22 | **2020 (abandoned)** | ❌ | ✅ |\n| leaky-bucket | ~800 | **2021 (abandoned)** | ❌ | ❌ |\n| limiter | ~65k | 2023 | partial | ✅ (sliding window, not leaky) |\n\n## Contributors ✨\n\nThis project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind are welcome — code, docs, bug reports, ideas, reviews! See the [emoji key](https://allcontributors.org/docs/en/emoji-key) for how each contribution is recognized, and open a PR or issue to get involved.\n\nThanks goes to these wonderful people:\n\n<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section -->\n<!-- prettier-ignore-start -->\n<!-- markdownlint-disable -->\n<table>\n  <tbody>\n    <tr>\n      <td align=\"center\" valign=\"top\" width=\"14.28%\"><a href=\"https://github.com/trananhtung\"><img src=\"https://avatars.githubusercontent.com/u/30992229?v=4?s=100\" width=\"100px;\" alt=\"Tung Tran\"/><br /><sub><b>Tung Tran</b></sub></a><br /><a href=\"https://github.com/trananhtung/./commits?author=trananhtung\" title=\"Code\">💻</a> <a href=\"#maintenance-trananhtung\" title=\"Maintenance\">🚧</a></td>\n    </tr>\n  </tbody>\n</table>\n\n<!-- markdownlint-restore -->\n<!-- prettier-ignore-end -->\n\n<!-- ALL-CONTRIBUTORS-LIST:END -->\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-ef3df141a8cf27e6e199f5babea0ffec"}