Many workers. One rate limit.
A TypeScript library for outbound API calls. Share one rate budget and provider pauses across every worker.
Beta until 1.0. APIs may change between releases.
README.md
Start local. Share when needed.
Start with an in-memory store for a script. Add Redis or Valkey when calls span processes. Same API, same guarantees: rate bounds apply at atomic store admission.
Installation guide$ pnpm add valvejs
import { createValve } from 'valvejs';
const api = createValve({
id: 'partner:credential-42',
rate: { limit: 100, interval: '1s' },
});
// GET retries after the provider's Retry-After; POST needs retryUnsafeMethods.
const limitedFetch = api.wrapFetch();
await limitedFetch('https://api.example.com/contacts');
await api.close({ drain: true });The Redis example expects your connected client. valve does not open or close it. Share a quota
Control the
shared budget.
Space starts, share quotas, and respect the provider’s backoff. Keep the scheduling rules in one place.
Read the guaranteesGCRA spaces starts; fixed windows cap aligned intervals. Several rules are checked atomically.
rate: [{ limit: 10, interval: '1s' }, { limit: 5000, interval: '1h', algorithm: 'fixed-window' }]Read the guide Spend business and per-recipient or per-tenant budgets atomically. Eligible identities proceed while others wait.
api.run(send, { scopes: { recipient: 'contact-42' } })Read the guide A Redis or Valkey store shares one quota across processes using the same credential.
store: redisStore({ client: redis })Read the guide Reads Retry-After and rate limit headers, then pauses every process sharing the valve.
api.observe({ status: 429, headers })Read the guide Retries 429s and 503s with a valid Retry-After for GET, HEAD, OPTIONS, PUT, and DELETE. POST and PATCH opt in.
api.wrapFetch(fetch, { retryUnsafeMethods: true })Read the guide Cap running work per process, or share one limit across workers through renewable leases.
concurrency: { limit: 4, scope: 'global' }Read the guide Job costs, queue bounds, wait deadlines, and priorities for interactive work.
api.run(() => sync(id), { priority: 10 })Read the guide Typed events for pauses, rejections, and retries, plus a stats() snapshot per valve.
api.on('pause', ({ durationMs }) => log(durationMs))Read the guide TypeScript types, ESM and CommonJS builds. It uses the Redis client you pass in.
dependencies: {}Read the guide Changes, in the open.
All changes- v0.6.1
latest
Patch Changes
- Add
runSequentialfor explicit, non-atomic composition of independent valves. Stages admit outermost first, each with its own run options and a shared abort signal. Earlier admissions stay spent when a later stage rejects, and outer jobs keep their concurrency slots and global leases while later stages wait. - Add
createValveGroupin the newvalvejs/groupentry for independent keyed valves on one shared store. Each key maps to the stable id${id}:${encodeURIComponent(key)}, so workers sharing a store share one budget per key, and different keys never share budgets, cooldowns, or concurrency. A per-key resolver snapshots configuration on first local use, and managed limits stay authoritative across eviction. A bounded cache (maxSize,idleTtl) plus bounded pending calls (maxPending) never evicts busy valves, evicts paused idle valves only when room is needed (their shared pause stays in the store), and rejects calls needing a new valve withQUEUE_FULLwhen every valve is busy.
- Add
- v0.6.0
published
Minor Changes
- Add opt-in runtime updates for existing named rate rules with
limits: 'managed',updateLimits(patch, { expect }),getLimits(), and thelimitsevent. Memory and Redis/Valkey stores enforce the committed numeric limits atomically, preserve fixed-window counts and GCRA cost-unit debt, and re-evaluate waiting jobs after increases. Structural changes and concurrency updates still require a coordinated migration. AddLIMITS_CONFLICTfor conditional update conflicts. Managed jobs that exceed the current rule capacity reject withCOST_EXCEEDS_LIMIT; unsafe virtual-time precision fails closed withINVALID_OPTIONS. Default immutable behavior is preserved. Managed metadata requires Redisnoeviction; idle scoped identities can recover at their old expiry after a rate decrease, and failover can lose acknowledged updates. See the runtime limits guide for migration and consistency limits. The additional managed implementation increases the core and Redis bundle sizes.
Patch Changes
- Move development benchmarks and bundle-size tooling into the private performance workspace and remove benchmark-only development dependencies from the library package. Runtime behavior and exports are unchanged.
- Add opt-in runtime updates for existing named rate rules with
- v0.5.2
published
Patch Changes
- Check positive maxWait deadlines immediately before invoking callbacks, including jobs admitted in the same batch. Skip expired or cancelled callbacks and release their reserved concurrency slots and global leases without counting them as started. Preserve maxWait: 0 as a single immediate admission attempt.
Built in the open.
Code, docs, benchmarks, and careful bug reports. Every contribution counts.