Async rate limiting keeps the window behavior described in the Rate Limiting Guide, while adding Promise results, retries, error callbacks, and control over in-flight work.
Use it when accepted operations return values you need, can reject, or need retry and abort support. Use the synchronous limiter when you only need an immediate accepted-or-rejected boolean.
Use asyncRateLimit when you only need a callable function:
import { asyncRateLimit } from '@tanstack/pacer'
const loadUser = asyncRateLimit(
async (id: string) => {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error('Request failed')
return response.json()
},
{ limit: 5, window: 60_000 },
)
const user = await loadUser('123')import { asyncRateLimit } from '@tanstack/pacer'
const loadUser = asyncRateLimit(
async (id: string) => {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) throw new Error('Request failed')
return response.json()
},
{ limit: 5, window: 60_000 },
)
const user = await loadUser('123')Use AsyncRateLimiter when you need methods, state, or callbacks:
import { AsyncRateLimiter } from '@tanstack/pacer'
const limiter = new AsyncRateLimiter(loadUserFromApi, {
limit: 5,
window: 60_000,
onReject: (args, limiter) => {
console.log(
`Rejected ${args[0]}; retry in ${limiter.getMsUntilNextWindow()}ms`,
)
},
})
const user = await limiter.maybeExecute('123')import { AsyncRateLimiter } from '@tanstack/pacer'
const limiter = new AsyncRateLimiter(loadUserFromApi, {
limit: 5,
window: 60_000,
onReject: (args, limiter) => {
console.log(
`Rejected ${args[0]}; retry in ${limiter.getMsUntilNextWindow()}ms`,
)
},
})
const user = await limiter.maybeExecute('123')maybeExecute() returns a Promise with two normal outcomes:
Use onReject or compare capacity before calling when undefined is also a valid function result.
if (limiter.getRemainingInWindow() > 0) {
const result = await limiter.maybeExecute('123')
}if (limiter.getRemainingInWindow() > 0) {
const result = await limiter.maybeExecute('123')
}Unlike the synchronous limiter, accepted calls can overlap. limit controls how many executions may start in the window, not how many may be active at once:
limit: 3
start A ───────────────── finish A
start B ───── finish B
start C ─────────────────── finish C
call D rejectedlimit: 3
start A ───────────────── finish A
start B ───── finish B
start C ─────────────────── finish C
call D rejectedUse an Async Queue when you need a concurrency limit or need excess work to wait instead of being rejected.
The window types match the synchronous limiter:
The default is fixed. getRemainingInWindow() reports available starts, and getMsUntilNextWindow() reports how long a rejected caller must wait for capacity.
An accepted execution consumes capacity when it starts. It still counts if the async function later fails or is aborted. Retries for that accepted execution do not add more rate-limit timestamps.
Async rate limiters provide callbacks for each outcome:
Rejection is not an execution error. It does not call onError or onSettled.
Without onError, throwOnError defaults to true, so a failed accepted execution rejects its Promise. Providing onError changes that default to false; the Promise then resolves with the current lastResult. Set throwOnError explicitly when you want different behavior.
Configure retries for each accepted execution with asyncRetryerOptions:
const limiter = new AsyncRateLimiter(sendRequest, {
limit: 5,
window: 60_000,
asyncRetryerOptions: {
maxAttempts: 3,
backoff: 'exponential',
baseWait: 1000,
jitter: 0.2,
},
})const limiter = new AsyncRateLimiter(sendRequest, {
limit: 5,
window: 60_000,
asyncRetryerOptions: {
maxAttempts: 3,
backoff: 'exponential',
baseWait: 1000,
jitter: 0.2,
},
})maxAttempts includes the first attempt. One accepted rate-limit slot may therefore produce multiple attempts against the downstream service. Account for that service's own limits before combining rate limiting and retries. See the Async Retrying Guide for retry safety.
abort() aborts all active executions. It does not remove their timestamps or restore window capacity. Pass each execution's signal to the underlying operation for cancellation to propagate:
const limiter = new AsyncRateLimiter(
async (id: string) => {
return fetch(`/api/users/${id}`, {
signal: limiter.getAbortSignal() ?? undefined,
})
},
{ limit: 5, window: 60_000 },
)
limiter.abort()const limiter = new AsyncRateLimiter(
async (id: string) => {
return fetch(`/api/users/${id}`, {
signal: limiter.getAbortSignal() ?? undefined,
})
},
{ limit: 5, window: 60_000 },
)
limiter.abort()When several executions overlap, getAbortSignal() without an argument refers to the most recently started execution. Pass its maybeExecuteCount to target a specific active execution.
reset() clears the rate-limit timestamps and restores default state. It does not guarantee that active underlying work stops, so abort first when a full cleanup is required:
limiter.abort()
limiter.reset()limiter.abort()
limiter.reset()Resetting restores capacity immediately. Only do this when starting a genuinely new limiting period, not as a way to bypass the configured limit.
enabled, limit, and window may be values or functions that receive the limiter instance. setOptions() merges new options into the current configuration.
limiter.setOptions({
enabled: (limiter) => limiter.store.state.errorCount < 3,
limit: (limiter) =>
limiter.store.state.rejectionCount > 10 ? 2 : 5,
})limiter.setOptions({
enabled: (limiter) => limiter.store.state.errorCount < 3,
limit: (limiter) =>
limiter.store.state.rejectionCount > 10 ? 2 : 5,
})Changing limit, window, or windowType does not erase existing execution history. Call reset() if a new configuration should begin with a fresh window. A disabled limiter does not execute the function or consume capacity; its calls resolve with undefined.
Use asyncRateLimiterOptions() to define reusable, type-checked option objects.
The class stores state at limiter.store.
To restore selected state that your app has persisted, pass a partial snapshot through initialState. It is merged with the defaults. Restore only durable fields. Pending timers and active executions are not restored.
Commonly useful properties include:
See the AsyncRateLimiter API reference for complete option and state types.