A rate limiter caps how many executions start per unit of time. Use it to stay under a third-party API quota (outbound), or to protect your service from one client sending too much (inbound, usually per key). It does not cap how many run at once: for that, use a concurrency limiter. Every limiter here is in-process; to share one limit between instances, plug in the Redis algorithm.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
ResiliencePipelineBuilder adds Polly-style addRetry() / addTimeout() / … shortcuts on top, at the cost of
bundling every strategy. Strategies run in the order they are added: the first one added is the outermost.
Options are validated and strategy state (e.g. circuit breaker) is created on build().
ResiliencePipelineBuilder adds Polly-style addRetry() / addTimeout() / … shortcuts on top, at the cost of
bundling every strategy. Strategies run in the order they are added: the first one added is the outermost.
Options are validated and strategy state (e.g. circuit breaker) is created on build().
Each request asks for one permit and nothing waits (no queue). The decisions come from the library's TokenBucket and WindowCounter. A red span means more than 8 requests got through inside one second.
Requests admitted per algorithm
Algorithm
Admitted
Rejected
Most admitted in any 1 s
Token bucket
16
5
11
Fixed window
21
0
16
Sliding window
13
8
8
Token bucket
Fixed window
Sliding window
Burst at once
tokenLimit
permitLimit
permitLimit
Worst case in one window-length span
tokenLimit + refills in that span
2 × permitLimit (edge burst)
permitLimit, to one segment’s precision
Quota comes back
tokensPerPeriod at each period
All at once at the boundary
One segment at a time
Cost per call
O(1)
O(1)
O(segmentsPerWindow)
Good for
Smooth outbound traffic with some burst
Simple quotas (“1000 per day”)
Inbound limits that must hold for any span
The edge burst. With a fixed window of 100 per minute, a client can send 100 requests at 0:59 and 100 more at 1:00: 200 in two seconds, all allowed. A sliding window (10 segments of 6 s) still counts the first 100 at 1:00, so the second 100 waits or is rejected. Use a sliding window when the limit protects something.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Weight of an execution (batch size, cost, tokens). Finite and ≥ 0. Above the limit’s maximum: rejected with PermitLimitExceededError.
queueTimeout
Duration
0 (reject at once)
Longest an execution waits for permits (same option as on the concurrency limiters, but here 0 means no waiting). Waiters are served first come, first served: a heavy request is never overtaken by lighter ones. If the permits can’t be free before the deadline, it is rejected right away instead of waiting in vain.
queueLimit
number
no limit
Most executions waiting (with queueTimeout); beyond it, rejected at once. Unlike the concurrency limiters (default 0), the rate limiters wait only when queueTimeout is set, so with the defaults nobody queues here either (as in Polly).
spacing
'even'
–
Delay granted executions so the quota left is spread evenly until the limit resets, instead of bursting at the start of the window.
minTime
Duration
0
Least time between the starts of two granted executions (bottleneck’s minTime). Also the floor of 'even' spacing.
blockDuration
Duration
0 (off)
Once an execution is rejected for exceeding the limit, reject everything for this long. Rejections during a block don’t extend it.
allow
(context) => boolean
–
Bypass the limiter (no permits taken), e.g. internal callers or health checks.
deny
(context) => boolean
–
Reject with RateLimiterDeniedError right away. Checked before allow.
control
RateLimiterControl
–
Manual penalty, reward, block, unblock and delete (see below).
Bucket size: the largest burst, and the tokens it starts with. Integer ≥ 1.
replenishmentPeriod
Duration
required
Refill interval. > 0.
tokensPerPeriod
number
tokenLimit
Tokens added each period (all at once, capped at tokenLimit). ≥ 1.
{ tokenLimit: 10, replenishmentPeriod: '1s' } refills the whole bucket every second. For a smoother rate, refill in small steps: { tokenLimit: 10, replenishmentPeriod: '100ms', tokensPerPeriod: 1 }.
Sliding only. Buckets the window slides by: more is smoother and more exact, at O(segments) per call.
alignment
'fixed' | 'first-request'
'fixed'
'fixed': windows follow each other from the limiter’s creation. 'first-request': a window starts at the first request after the previous one emptied (per key when partitioned), like “100 per hour from your first call”.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Shared bag for passing data between strategies and the callback.
properties.
Map<string, unknown>.get(key: string): unknown
Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
@returns ― Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
Longest an execution may wait for permits before it is rejected. Waiters are served first come, first served. Default 0: reject right away.
queueTimeout: '30s',
PermitRateLimiterOptions.queueLimit?: number |undefined
Most executions waiting (with queueTimeout); beyond it they are rejected at once. Default: no limit, as queueTimeout (default 0) already decides whether anyone waits.
A window limiter lets the whole quota through at the start of the window. Many APIs dislike that. spacing: 'even' delays each granted execution so the rest of the quota is spread until the limit resets: 4 per second runs at 0, 250, 500 and 750 ms. minTime sets a fixed gap between starts instead (or a floor under even spacing).
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
'even': delays granted executions so the quota left is spread evenly until the limit resets, instead of
bursting at the start of the window. The delay counts against queueTimeout when that is set; abort ends it.
The pacing delay counts against queueTimeout when it is set: an execution that would be paced past its queueTimeout deadline starts at the deadline instead. An abort during the delay ends it. The permit is not refunded in that case.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Shared bag for passing data between strategies and the callback.
properties.
Map<string, unknown>.get(key: string): unknown
Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
@returns ― Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
A partition unused this long (and with nothing running) is dropped, resetting its limit. Default 10 minutes.
Keep it at least as long as the limiter's window, or an evicted partition gets a fresh quota early.
idleTimeout: '10m',
PartitionedRateLimiterOptions<any>.maxPartitions?: number |undefined
Most partitions kept; past it the least recently used partition with nothing running is dropped (its limit resets).
When all of them are running, a new key is rejected with RateLimiterRejectedError. Default: no limit.
new <string, string>(iterable?:Iterable<readonly [string, string]> |null|undefined) =>Map<string, string> (+3 overloads)
Map([['apiKey', 'key_123']]) });
Option
Type
Default
Description
partitionKey
(context) => string
required
Which partition the execution counts against. If it throws, the execution fails with that error.
limiter
(key, source) => ResilienceStrategy
required
Creates the limiter of a new partition. Any strategy works; control and snapshot need RateLimiterStrategy partitions (fixed, sliding, token bucket, custom algorithm).
idleTimeout
Duration
'10m'
A partition unused this long (with nothing running) is dropped, resetting its limit. Keep it ≥ the window, or an evicted key gets a fresh quota early.
maxPartitions
number
no limit
Most partitions kept; past it, the least recently used idle partition is dropped. When every partition is running, a new key is rejected with RateLimiterRejectedError instead, so memory stays bounded. Set it for keys you don’t control (IPs).
control
RateLimiterControl
–
Penalty / reward / block per key.
name
string
'PartitionedRateLimiter'
Strategy name.
The strategy instance (getStrategy<PartitionedRateLimiterStrategy<unknown>>('PartitionedRateLimiter')) exposes partitions (count), keys() (least recently used first) and delete(key?) (drop one or every partition: fresh quota, no block).
RateLimiterControl changes a limiter’s state from outside. blockDuration blocks automatically once the limit is hit. Together they cover brute-force protection:
const
constcontrol:RateLimiterControl
control=new
newRateLimiterControl(): RateLimiterControl
Manual control of the rate limiters it is passed to (control option): penalty, reward, block, unblock, delete.
key picks a partition of a partitioned limiter (omitted: every partition); single limiters ignore it.
Pass it to the partitioned limiter, not to the per-key limiters it creates.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
A partition unused this long (and with nothing running) is dropped, resetting its limit. Default 10 minutes.
Keep it at least as long as the limiter's window, or an evicted partition gets a fresh quota early.
idleTimeout: '1h', // >= the block, or an evicted partition forgets it
Penalty / reward / block per key. The per-key limiters must be RateLimiterStrategys (fixed, sliding, token bucket). A block is lost if its partition is evicted, so keep idleTimeout >= the block.
user); // a wrong password costs 3 permits in total
return
constok:boolean
ok;
}
Method
Effect
penalty(points, key?)
Charge points permits without an admission check (a token bucket can go negative).
reward(points, key?)
Give points back; Infinity resets the count. Wakes waiters.
block(duration, key?)
Reject everything for duration.
unblock(key?)
End a block.
delete(key?)
Fresh quota and no block (drops the partition when partitioned).
key picks a partition; omit it to act on every partition. Single limiters ignore key. Pass control to the partitioned limiter, not to the per-key limiters it creates. penalty and block create a missing partition; reward and unblock don’t. An algorithm without adjust throws a TypeError on penalty / reward.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Shared bag for passing data between strategies and the callback.
properties.
Map<string, unknown>.get(key: string): unknown
Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
@returns ― Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
Shared bag for passing data between strategies and the callback.
properties.
Map<string, unknown>.get(key: string): unknown
Returns a specified element from the Map object. If the value that is associated to the provided key is an object, then you will get a reference to that object and any change made to that object will effectively modify it inside the Map.
@returns ― Returns the element associated with the specified key. If no element is associated with the specified key, undefined is returned.
get('ip') ==='10.0.0.1', // no limit, no permits taken
After a limiter admits or rejects an execution, getRateLimitInfo(context) returns { limit, remaining, resetAfter } (resetAfter in ms; on rejection, until a retry may pass). Read it inside the callback, or pass your own properties map and read it afterwards. The /http, Fastify and Koa middlewares turn it into RateLimit-* headers for you.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
ResiliencePipelineBuilder,
functiongetRateLimitInfo(context: {
readonlyproperties:Map<string, unknown>;
}):RateLimitInfo|undefined
Limit, remaining permits and reset time the rate limiter reported for this execution, e.g. for RateLimit-* headers.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
BurstyRateLimiter(primary, burst) combines a steady limit with a burst allowance. burst is consulted only when primary rejects: “10 per second, plus up to 20 extra per minute”. Run it with RateLimiterStrategy:
import {
classBurstyRateLimiter
A steady limit plus a burst allowance: burst is only consulted when primary rejects, e.g. 10/s that may spill
into 20 extra per minute. Any algorithms work, sync or async (in-memory, Redis):
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
ResiliencePipelineBuilder,
classWindowCounter
Segmented sliding window: the window is split into segments buckets and slides one bucket at a time.
With 1 segment it is a fixed window. Windows are aligned to origin; with 'first-request' alignment a new
window starts at the first request after the window empties instead.
WindowCounter } from'@ylcnfrht/resilience';
const
constnow:number
now=
var Date:DateConstructor
Enables basic storage and retrieval of dates and times.
Date.
DateConstructor.now(): number
Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
A steady limit plus a burst allowance: burst is only consulted when primary rejects, e.g. 10/s that may spill
into 20 extra per minute. Any algorithms work, sync or async (in-memory, Redis):
Segmented sliding window: the window is split into segments buckets and slides one bucket at a time.
With 1 segment it is a fixed window. Windows are aligned to origin; with 'first-request' alignment a new
window starts at the first request after the window empties instead.
Segmented sliding window: the window is split into segments buckets and slides one bucket at a time.
With 1 segment it is a fixed window. Windows are aligned to origin; with 'first-request' alignment a new
window starts at the first request after the window empties instead.
Both parts can be any RateLimiterAlgorithm, sync or async (in-memory or Redis). permitLimit is the larger of the two, so a weight only the burst pool can hold still gets in. Penalty / reward hit the primary; a full reset (control.delete()) also refills the burst pool.
With a remote algorithm (Redis), a store outage would fail every call. insurance answers instead when the remote acquire throws or takes longer than timeout, and reports OnRateLimiterInsuranceUsed. Size it per process: the shared limit divided by the number of instances.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Longest an execution may wait for permits before it is rejected. Waiters are served first come, first served. Default 0: reject right away.
queueTimeout: '1s',
PermitRateLimiterOptions.insurance?: {
algorithm: RateLimiterAlgorithm;
timeout?: Duration;
} |undefined
In-memory fallback for a remote algorithm: when acquire throws or exceeds timeout, algorithm answers instead
and OnRateLimiterInsuranceUsed is reported. Size it per process (e.g. the shared limit / instances).
Segmented sliding window: the window is split into segments buckets and slides one bucket at a time.
With 1 segment it is a fixed window. Windows are aligned to origin; with 'first-request' alignment a new
window starts at the first request after the window empties instead.
WindowCounter(250, 60_000, 10,
var Date:DateConstructor
Enables basic storage and retrieval of dates and times.
Date.
DateConstructor.now(): number
Returns the number of milliseconds elapsed since midnight, January 1, 1970 Universal Coordinated Time (UTC).
In serverless or across deploys, a fresh limiter means a fresh quota. snapshot() returns JSON-serializable state; restore() loads it, aging it by the time passed (windows expire, tokens refill, blocks run out). The partitioned limiter snapshots per key, snapshot(n) keeping the n most recently used.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Loads a snapshot(); time passed since it was taken counts (windows expire, tokens refill, blocks run out).
restore(
varJSON:JSON
An intrinsic object that provides functions to convert JavaScript values to and from the JavaScript Object Notation (JSON) format.
JSON.
JSON.parse(text: string, reviver?: (this:any, key:string, value:any) => any): any
Converts a JavaScript Object Notation (JSON) string into an object.
@param ― text A valid JSON string.
@param ― reviver A function that transforms the results. This function is called for each member of the object.
If a member contains nested objects, the nested objects are transformed before the parent object is.
@throws ― {SyntaxError} If text is not valid JSON.
parse(
constsaved:string
saved)); // throws TypeError on a malformed snapshot
Every limiter is RateLimiterStrategy (waiting, weights, block, allow/deny, insurance, telemetry) around a RateLimiterAlgorithm (admission only). Implement the interface to bring your own, e.g. over a database; acquire may be async.
import { type
typeAcquireResult= {
readonlyok:true;
readonlyremaining?:number;
readonlyresetAfter?:number;
} | {
readonlyok:false;
readonlyretryAfter:number;
}
On success, remaining and resetAfter (ms until everything used so far is released) feed getRateLimitInfo, when known.
AcquireResult, type
(alias) interfaceRateLimiterAlgorithm
importRateLimiterAlgorithm
The admission logic of a rate limiter, without waiting or telemetry. The in-memory WindowCounter implements it;
implement it over Redis (one Lua script per acquire) to share a limit between processes. acquire may be async.
RateLimiterAlgorithm,
classRateLimiterStrategy<T>
Runs any RateLimiterAlgorithm as a strategy: weights, FIFO waiting up to queueTimeout, block, allow/deny, insurance, telemetry.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
The admission logic of a rate limiter, without waiting or telemetry. The in-memory WindowCounter implements it;
implement it over Redis (one Lua script per acquire) to share a limit between processes. acquire may be async.
@param ― start The index to the beginning of the specified portion of stringObj.
@param ― end The index to the end of the specified portion of stringObj. The substring includes the characters up to, but not including, the character indicated by end.
If this value is not specified, the substring continues to the end of stringObj.
slice(0, 10);
if (
constday:string
day!==this.#day) {
this.#day =
constday:string
day;
this.#used =0;
}
const
constresetAfter:number
resetAfter=
var Date:DateConstructor
Enables basic storage and retrieval of dates and times.
Date.
DateConstructor.parse(s: string): number
Parses a string containing a date, and returns the number of milliseconds between that date and midnight, January 1, 1970.
Polly-style builder with a shortcut for every strategy: addRetry(), addTimeout(), addCircuitBreaker(), …
Convenient, but it bundles every strategy. For the smallest browser bundle use PipelineBuilder with
retryStrategy(), timeoutStrategy(), … instead; the two are interchangeable.
The type parameters track which errors the pipeline can raise, for pipeline.settle() + Outcome.match().
Largest weight ever granted. Larger requests are rejected with PermitLimitExceededError before acquire.
acquire(permits, now)
yes
Take the permits if they fit: { ok: true, remaining?, resetAfter? } (both needed for getRateLimitInfo), or { ok: false, retryAfter } in ms. A rejection must take nothing.
adjust(points, now)
no
Charge (> 0) or refund (< 0) without an admission check; -Infinity empties. Enables penalty / reward / delete.
snapshot(now) / restore(state, now)
no
Enable snapshot() / restore().
WindowCounter and TokenBucket are exported for reuse (insurance, bursty).
The remote algorithm failed or timed out; insurance answered. Args: { context, error }.
Error
code
HTTP
When
RateLimiterRejectedError
RATE_LIMITED
429
Over the limit (retryAfter in ms), blocked, or the wait queue is full (retryAfter undefined).
PermitLimitExceededError
PERMIT_LIMIT_EXCEEDED
429
permits above the limiter’s maximum: waiting can never help. Has permits and permitLimit.
RateLimiterDeniedError
RATE_LIMIT_DENIED
403
Matched deny.
All three extend RateLimiterRejectedError, so isRateLimitError() and isRejection() match them. The default retry predicate does not retry rejections. To wait out a limit instead, use queueTimeout. For outbound quotas see calling rate-limited APIs.