Features

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:

  1. Status check: Is the status code in the waitOnStatus list?
  2. Attempt check: Have we exceeded maxWaitAttempts?
  3. Extract wait time: Read from Retry-After header, custom header, response body, or custom function
  4. Max wait check: Is the extracted wait time within maxWaitTime?
  5. Fire hook: Call onRateLimitWait hooks for monitoring
  6. Sleep: Wait the extracted duration
  7. 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: 30 is parsed as 30 seconds (30000ms)
  • HTTP-date: Retry-After: Thu, 01 Jan 2026 00:00:00 GMT computes 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:

  1. Rate limit wait runs first. When a 429 response is received, the wait system handles it.
  2. If waits are exhausted, the response enters the retry system (if configured).
  3. If retry eventually succeeds, your code sees the successful response.
  4. 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)