Skip to content

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.

worker-01worker-02worker-03ingressqueued 00/7 admitted 2 req/s
Illustrative flow

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 guarantees

GCRA 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

Changes, in the open.

All changes
  1. v0.6.1

    latest

    Patch Changes

    • Add runSequential for 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 createValveGroup in the new valvejs/group entry 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 with QUEUE_FULL when every valve is busy.
  2. v0.6.0

    published

    Minor Changes

    • Add opt-in runtime updates for existing named rate rules with limits: 'managed', updateLimits(patch, { expect }), getLimits(), and the limits event. 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. Add LIMITS_CONFLICT for conditional update conflicts. Managed jobs that exceed the current rule capacity reject with COST_EXCEEDS_LIMIT; unsafe virtual-time precision fails closed with INVALID_OPTIONS. Default immutable behavior is preserved. Managed metadata requires Redis noeviction; 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.
  3. 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.