Rate Limit Waiting
Rezo can automatically detect rate-limited responses (HTTP 429 and others), extract the wait time, sleep, and retry the request — all before the retry system even kicks in. This makes handling rate limits seamless: your code sees the successful response without any manual sleep-and-retry logic.
Quick Start
import { Rezo } from 'rezo';
// Enable automatic waiting on 429 responses
const { data } = await rezo.get('https://api.example.com/data', {
waitOnStatus: true
});
// If the server returns 429 with Retry-After: 5,
// Rezo waits 5 seconds and retries automatically. How It Works
The rate limit wait feature runs before the retry system. When a response comes back with a rate-limiting status code:
- Status check: Is the status code in the
waitOnStatuslist? - Attempt check: Have we exceeded
maxWaitAttempts? - Extract wait time: Read from
Retry-Afterheader, custom header, response body, or custom function - Max wait check: Is the extracted wait time within
maxWaitTime? - Fire hook: Call
onRateLimitWaithooks for monitoring - Sleep: Wait the extracted duration
- Retry: Send the request again
If waiting succeeds, your code receives the final successful response. If all wait attempts are exhausted, the response flows into the normal retry system (if configured) or throws an error.
Configuration
waitOnStatus
Controls which HTTP status codes trigger the wait-and-retry behavior.
// Enable for 429 only (default behavior)
waitOnStatus: true
// Enable for specific status codes
waitOnStatus: [429, 503]
// Disable
waitOnStatus: false // or omit entirely Default wait status codes (when true): [429]
waitTimeSource
Where to extract the wait time from the response. Defaults to the standard Retry-After header.
Standard Retry-After Header (Default)
Parses the Retry-After header, supporting both delta-seconds and HTTP-date formats:
// Retry-After: 5 -> wait 5 seconds
// Retry-After: 120 -> wait 120 seconds
// Retry-After: Thu, 01 Jan 2026 00:00:00 GMT -> wait until that date
waitTimeSource: 'retry-after' // This is the default The parser handles both formats automatically:
- Delta-seconds:
Retry-After: 30is parsed as 30 seconds (30000ms) - HTTP-date:
Retry-After: Thu, 01 Jan 2026 00:00:00 GMTcomputes the difference from now
Custom Header
Extract wait time from a non-standard header. The value is parsed as seconds. If the value looks like a Unix timestamp (larger than current epoch but within a year), it is interpreted as an absolute timestamp and the wait time is computed as the difference from now.
// X-RateLimit-Reset: 30 -> wait 30 seconds
waitTimeSource: { header: 'X-RateLimit-Reset' }
// X-RateLimit-Reset: 1735689600 -> wait until that Unix timestamp
waitTimeSource: { header: 'X-RateLimit-Reset' } await rezo.get(url, {
waitOnStatus: true,
waitTimeSource: { header: 'X-RateLimit-Reset' }
}); Response Body Path
Extract wait time from a field in the JSON response body using dot notation. The value is interpreted as seconds.
// Response: { "error": { "retry_after": 10 } }
waitTimeSource: { body: 'error.retry_after' }
// Response: { "wait_seconds": 30 }
waitTimeSource: { body: 'wait_seconds' }
// Response: { "data": { "rate_limit": { "reset_in": 5 } } }
waitTimeSource: { body: 'data.rate_limit.reset_in' } await rezo.get(url, {
waitOnStatus: true,
waitTimeSource: { body: 'retry_after' }
}); Custom Function
For complex APIs, provide a function that receives the response and returns the number of seconds to wait (or null to fall back to the default wait time).
await rezo.get(url, {
waitOnStatus: [429, 503],
waitTimeSource: (response) => {
// Custom logic: compute wait from X-RateLimit-Reset header
const reset = response.headers.get('x-ratelimit-reset');
if (reset) {
const resetTime = parseInt(reset, 10);
const now = Math.floor(Date.now() / 1000);
return resetTime - now; // seconds to wait
}
// Fall back to body field
if (response.data?.retry_after) {
return response.data.retry_after;
}
return null; // use defaultWaitTime
}
}); The function signature:
(response: { status: number; headers: RezoHeaders; data?: any }) => number | null Return null or throw to fall back to defaultWaitTime.
maxWaitAttempts
Maximum number of times to wait and retry before giving up. After this many waits, the response flows into the normal retry system or fails.
maxWaitAttempts: 5 // Will wait up to 5 times Default: 3
maxWaitTime
Maximum time to wait for a single rate limit response in milliseconds. If the extracted wait time exceeds this, the request fails immediately instead of waiting. Set to 0 for unlimited.
maxWaitTime: 120000 // Never wait more than 2 minutes Default: 60000 (60 seconds)
defaultWaitTime
Fallback wait time in milliseconds when the configured source returns nothing (e.g., no Retry-After header present, or the body path doesn’t exist).
defaultWaitTime: 2000 // Wait 2 seconds when no explicit time is given Default: 1000 (1 second)
Full Configuration Example
const client = new Rezo({
// Instance-level rate limit config
waitOnStatus: [429, 503],
waitTimeSource: 'retry-after',
maxWaitAttempts: 3,
maxWaitTime: 60000,
defaultWaitTime: 1000
});
// Override per-request
const { data } = await client.get('/api/search', {
waitOnStatus: true,
waitTimeSource: { header: 'X-RateLimit-Reset' },
maxWaitAttempts: 5,
maxWaitTime: 120000
}); The onRateLimitWait Hook
The onRateLimitWait hook fires each time Rezo is about to wait due to rate limiting. It is informational — you cannot abort the wait from this hook. Use it for monitoring, alerting, or logging.
const client = new Rezo({
waitOnStatus: true,
hooks: {
onRateLimitWait: [
(event, config) => {
console.log(
`Rate limited: ${event.status} on ${event.method} ${event.url}
` +
` Wait: ${event.waitTime}ms (attempt ${event.attempt}/${event.maxAttempts})
` +
` Source: ${event.source}${event.sourcePath ? ` (${event.sourcePath})` : ''}`
);
}
]
}
}); RateLimitWaitEvent
interface RateLimitWaitEvent {
/** HTTP status code that triggered the wait (e.g., 429, 503) */
status: number;
/** Time to wait in milliseconds */
waitTime: number;
/** Current wait attempt number (1-indexed) */
attempt: number;
/** Maximum wait attempts configured */
maxAttempts: number;
/** Where the wait time was extracted from */
source: 'header' | 'body' | 'function' | 'default';
/** The header name or body path used (if applicable) */
sourcePath?: string;
/** URL being requested */
url: string;
/** HTTP method */
method: string;
/** Timestamp when the wait started */
timestamp: number;
} Interaction with Retry
Rate limit waiting and retry are complementary features that work together:
- Rate limit wait runs first. When a 429 response is received, the wait system handles it.
- If waits are exhausted, the response enters the retry system (if configured).
- If retry eventually succeeds, your code sees the successful response.
- If both systems exhaust their attempts, the final error is thrown.
const { data } = await rezo.get(url, {
// Rate limit waiting: up to 3 waits on 429
waitOnStatus: true,
maxWaitAttempts: 3,
// Retry: up to 2 retries for other failures
retry: {
maxRetries: 2,
retryDelay: 1000,
backoff: 'exponential',
statusCodes: [500, 502, 503, 504]
}
}); In this setup, a 429 response triggers rate limit waiting (up to 3 times). A 500 response triggers retries (up to 2 times). A 429 that exhausts all 3 wait attempts would then enter the retry system for up to 2 more attempts.
Practical Examples
GitHub API
GitHub uses Retry-After and X-RateLimit-Reset headers:
const github = new Rezo({
baseURL: 'https://api.github.com',
headers: { Accept: 'application/vnd.github.v3+json' },
waitOnStatus: [403, 429], // GitHub uses 403 for rate limits too
waitTimeSource: (response) => {
// Check Retry-After first
const retryAfter = response.headers.get('retry-after');
if (retryAfter) return parseInt(retryAfter, 10);
// Fall back to X-RateLimit-Reset (Unix timestamp)
const reset = response.headers.get('x-ratelimit-reset');
if (reset) {
return parseInt(reset, 10) - Math.floor(Date.now() / 1000);
}
return null;
},
maxWaitTime: 300000, // Up to 5 minutes
maxWaitAttempts: 2
});
const { data } = await github.get('/repos/owner/repo/issues'); Stripe API
Stripe returns wait time in the response body:
const stripe = new Rezo({
baseURL: 'https://api.stripe.com/v1',
waitOnStatus: [429],
waitTimeSource: { body: 'error.retry_after' },
maxWaitAttempts: 3
}); Monitoring Rate Limit Patterns
const client = new Rezo({
waitOnStatus: true,
hooks: {
onRateLimitWait: [
(event) => {
metrics.increment('http.rate_limited', {
status: String(event.status),
source: event.source,
url: new URL(event.url).pathname
});
metrics.histogram('http.rate_limit_wait_ms', event.waitTime);
if (event.attempt >= event.maxAttempts) {
alerting.warn(`Rate limit exhausted for ${event.url}`);
}
}
]
}
}); Debug Logging
Enable debug: true on the request to see rate limit wait decisions in the console:
const { data } = await rezo.get(url, {
waitOnStatus: true,
debug: true
});
// Console output:
// [Rezo Debug] Rate limit (429) - waiting 5000ms (attempt 1/3, source: header:retry-after)