valve_

Choose an algorithm

Match a GCRA or fixed-window rule to the quota your provider documents.

Read your provider's rate limit documentation first, then pick the rule that describes it. Keep the default GCRA rule with burst: 1 for pacing. Choose fixed-window explicitly when the provider counts matching calendar windows. Exact rolling-window quotas are unsupported. GCRA spaces admissions; it does not keep an exact history of requests in a moving time window.

Provider saysRule
"100 requests per second"{ limit: 100, interval: '1s' }
"100 per second, bursts up to 20 allowed"{ limit: 100, interval: '1s', burst: 20 }
"10,000 per day, resets at midnight UTC"{ limit: 10000, interval: '24h', algorithm: 'fixed-window' }
"1,000 per minute, counted per calendar minute"{ limit: 1000, interval: '1m', algorithm: 'fixed-window' }
"10 per second and 100,000 per day"An array of two rules. See Combine rules.

GCRA

GCRA (the generic cell rate algorithm) is the default. It spaces starts evenly at interval / limit per cost unit.

rate: { limit: 10, interval: '1s' }

This starts one unit every 100ms. In any window of one second, at most 10 units start.

Allow bursts

burst lets that many units start back to back after a quiet period.

rate: { limit: 10, interval: '1s', burst: 5 }

After idle time, 5 units can start together, then spacing resumes. The bound in any one-second window becomes limit + burst - 1, which is 14 here. Only raise burst when the provider tolerates it.

Cost must fit in the burst

Under GCRA, a job with cost: 3 needs burst of at least 3. Otherwise run rejects it with COST_EXCEEDS_LIMIT, since it could never start.

Fixed window

A fixed-window rule allows limit units in each aligned window [k × interval, (k + 1) × interval) on the store's epoch clock.

rate: { limit: 100, interval: '1m', algorithm: 'fixed-window' }

Valve computes the window index as Math.floor(storeTimeMs / intervalMs). The epoch is 1970-01-01 00:00:00 UTC, so a 24h interval ends at UTC midnight and a 1m interval ends at a UTC minute boundary. This deterministic alignment lets every worker agree on a reset using the store clock, regardless of process startup time or the worker's timezone. It does not use the host's local midnight.

Arbitrary intervals are epoch-aligned, not necessarily calendar-aligned: 7h is a sequence of seven-hour blocks from the epoch. Local-time calendar resets, custom reset offsets, months, and daylight-saving adjustments are unsupported.

For a daily limit of 100, 100 admissions at 23:59:59.999 UTC and another 100 at 00:00:00.000 UTC are permitted. A request at the boundary belongs to the new window. Setting burst does not change fixed-window behavior; it is a GCRA option.

Near a boundary, the end of one window and the start of the next can admit up to 2 × limit units close together. Use fixed windows only when the provider counts the same way.

Exact rolling windows are unsupported

An exact rolling limit of 100 requests per 24 hours counts admissions in the preceding 24 hours at each decision. An admission stops counting exactly 24 hours later; there is no shared midnight reset. This needs timestamp history or an equivalent exact representation that Valve does not implement.

Fixed-window resets an entire counter at each aligned boundary. GCRA maintains a theoretical arrival time, drains accumulated pacing debt continuously, and allows an explicit burst. Neither implements an exact rolling-window counter. For example, a GCRA rule with limit: 100, interval: '24h', burst: 1 spaces unit-cost admissions 14.4 minutes apart. An exact rolling counter could instead allow all 100 immediately when empty, then block further admissions until old ones leave the window. Raising GCRA's burst changes its bound; it does not turn it into that rolling counter.

Use GCRA to pace work, fixed-window for a provider's matching calendar quota, and a different limiter if exact rolling-window semantics are required. These are admission guarantees, not measurements of provider arrival times.

Combine rules

Pass an array when a provider has several quotas. Each rule is enforced independently, and each job's cost is spent against every rule.

rate: [
  { limit: 10, interval: '1s' },
  { limit: 100_000, interval: '24h', algorithm: 'fixed-window' },
];

The store checks all rules in one atomic step. If any rule rejects a job, none of its cost is spent.

Unnamed rules cannot share the same algorithm and interval. Give each rule a unique name to distinguish otherwise matching rules. Scoped rules require names.

Changing rules

Unnamed rule state is keyed by algorithm and interval; named rules use their names, and scoped rules also include the job identity. State keys alone do not make a policy change compatible: Redis checks a persistent fingerprint of the complete policy before every admission.

Changing a name, scope dimension, algorithm, interval, limit, burst, or global lease setting rejects incompatible workers with INVALID_OPTIONS. Adding or removing a rule also changes the fingerprint. Reordering identical rules or using equivalent duration syntax is safe during a rolling deployment.

For policy changes, stop and drain old workers, allow existing budgets and the provider quota to recover, then reset only the fingerprint and deploy one consistent policy. Follow the practical quota migration procedure. Do not delete active rate keys or introduce a fresh namespace to bypass spent quota.

On this page