Skip to content

Rate Limiting

The iRacing API enforces rate limits to ensure fair usage and system stability. The SDK detects rate limit responses and throws an IRacingError with isRateLimited = true. By default it does not retry failed requests. You can opt in to automatic retry with backoff using the retry option, and you can opt in to local request throttling with a pluggable stores.rateLimit implementation.

Instead of building your own rate limiter, you can provide a store that the SDK’s underlying HttpClient will use to throttle requests automatically:

import { IRacingDataClient } from 'iracing-data-client';
import { InMemoryRateLimitStore } from '@http-client-toolkit/store-memory';
const iracing = new IRacingDataClient({
auth: { /* ... */ },
stores: {
rateLimit: new InMemoryRateLimitStore({
defaultConfig: { limit: 100, windowMs: 60_000 }
})
},
rateLimit: {
throw: false,
maxWaitTime: 60_000
}
});
// Requests now wait locally when the configured store is exhausted
const data = await iracing.member.info();

The SDK scopes toolkit rate-limit buckets by request URL origin. For normal iRacing Data API calls, the resource key is https://members-ng.iracing.com.

The stores option is completely optional. When omitted, the SDK behaves exactly as before.

Check the http-client-toolkit repository for available store packages, or implement the RateLimitStore interface yourself.

The SDK throws an IRacingError when the API returns a 429 status:

import { IRacingDataClient, IRacingError } from 'iracing-data-client';
try {
const data = await iracing.track.get();
} catch (error) {
if (error instanceof IRacingError && error.isRateLimited) {
console.log('Rate limit exceeded!');
console.log('Status:', error.status); // 429
}
}

Enable built-in retry for transient failures, including 429 and 5xx responses:

import { DEFAULT_RETRY_OPTIONS, IRacingDataClient } from 'iracing-data-client';
const iracing = new IRacingDataClient({
auth: { /* ... */ },
retry: DEFAULT_RETRY_OPTIONS
});
const data = await iracing.track.get();

If you do not provide a stores.rateLimit implementation, you can wrap calls with your own concurrency control:

class RateLimiter {
private queue: Array<() => void> = [];
private running = 0;
constructor(
private maxConcurrent = 2,
private minTime = 100 // ms between requests
) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
await this.waitForSlot();
this.running++;
const startTime = Date.now();
try {
return await fn();
} finally {
const elapsed = Date.now() - startTime;
if (elapsed < this.minTime) {
await new Promise((r) => setTimeout(r, this.minTime - elapsed));
}
this.running--;
this.processQueue();
}
}
private waitForSlot(): Promise<void> {
if (this.running < this.maxConcurrent) {
return Promise.resolve();
}
return new Promise((resolve) => {
this.queue.push(resolve);
});
}
private processQueue() {
if (this.queue.length > 0 && this.running < this.maxConcurrent) {
const next = this.queue.shift();
next?.();
}
}
}
// Usage
const limiter = new RateLimiter(2, 100);
const results = await Promise.all([
limiter.execute(() => iracing.car.get()),
limiter.execute(() => iracing.track.get()),
limiter.execute(() => iracing.series.get()),
]);

For more explicit request pacing, use a token bucket wrapper:

class TokenBucket {
private tokens: number;
private lastRefill: number;
constructor(
private capacity = 10,
private refillRate = 1, // tokens per second
private refillInterval = 1000 // ms
) {
this.tokens = capacity;
this.lastRefill = Date.now();
}
async acquire(tokens = 1): Promise<void> {
await this.refill();
while (this.tokens < tokens) {
await new Promise((r) => setTimeout(r, this.refillInterval));
await this.refill();
}
this.tokens -= tokens;
}
private async refill() {
const now = Date.now();
const elapsed = now - this.lastRefill;
const tokensToAdd = Math.floor((elapsed / 1000) * this.refillRate);
if (tokensToAdd > 0) {
this.tokens = Math.min(this.capacity, this.tokens + tokensToAdd);
this.lastRefill = now;
}
}
getAvailableTokens(): number {
return this.tokens;
}
}
// Usage
const bucket = new TokenBucket(10, 2); // 10 tokens, 2/sec refill
async function makeRequest() {
await bucket.acquire(1);
return iracing.track.get();
}

Reduce API calls by batching requests:

// Instead of multiple individual requests
const member1 = await iracing.member.get({ custIds: [123456] });
const member2 = await iracing.member.get({ custIds: [789012] });
// Use a single batched request (up to 50 IDs)
const allMembers = await iracing.member.get({
custIds: [123456, 789012, 345678],
});

If you need queue semantics in addition to throttling, implement them at the application layer:

class RequestQueue {
private queue: Array<{
fn: () => Promise<any>;
resolve: (value: any) => void;
reject: (error: any) => void;
}> = [];
private processing = false;
private requestCount = 0;
private windowStart = Date.now();
constructor(
private maxRequests = 100,
private windowMs = 60000 // 1 minute
) {}
async add<T>(fn: () => Promise<T>): Promise<T> {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this.process();
});
}
private async process() {
if (this.processing) return;
this.processing = true;
while (this.queue.length > 0) {
if (this.shouldWait()) {
await this.waitForReset();
}
const item = this.queue.shift();
if (!item) break;
try {
const result = await item.fn();
item.resolve(result);
this.requestCount++;
} catch (error) {
if (error instanceof IRacingError && error.isRateLimited) {
this.queue.unshift(item);
await this.waitForReset();
} else {
item.reject(error);
}
}
// Small delay between requests
await new Promise((r) => setTimeout(r, 50));
}
this.processing = false;
}
private shouldWait(): boolean {
const now = Date.now();
if (now - this.windowStart > this.windowMs) {
this.windowStart = now;
this.requestCount = 0;
return false;
}
return this.requestCount >= this.maxRequests;
}
private async waitForReset() {
const waitTime = this.windowMs - (Date.now() - this.windowStart);
console.log(`Rate limit reached. Waiting ${waitTime}ms`);
await new Promise((r) => setTimeout(r, waitTime));
this.windowStart = Date.now();
this.requestCount = 0;
}
}
// Usage
const queue = new RequestQueue(100, 60000);
const data = await queue.add(() => iracing.track.get());

Track and log rate limit usage:

class RateLimitMonitor {
private attempts = 0;
private rateLimitHits = 0;
private successfulRequests = 0;
private startTime = Date.now();
async execute<T>(fn: () => Promise<T>): Promise<T> {
this.attempts++;
try {
const result = await fn();
this.successfulRequests++;
return result;
} catch (error) {
if (error instanceof IRacingError && error.isRateLimited) {
this.rateLimitHits++;
console.warn(`Rate limit hit! Total: ${this.rateLimitHits}`);
}
throw error;
}
}
getStats() {
const elapsed = (Date.now() - this.startTime) / 1000;
const requestsPerSecond = this.successfulRequests / elapsed;
return {
attempts: this.attempts,
successful: this.successfulRequests,
rateLimitHits: this.rateLimitHits,
failureRate: `${((this.rateLimitHits / this.attempts) * 100).toFixed(2)}%`,
requestsPerSecond: requestsPerSecond.toFixed(2),
uptime: `${elapsed.toFixed(0)}s`,
};
}
}
// Usage
const monitor = new RateLimitMonitor();
const data = await monitor.execute(() => iracing.track.get());
// Check stats periodically
setInterval(() => {
console.log('Rate Limit Stats:', monitor.getStats());
}, 30000);

Cache Aggressively

Use caching to reduce unnecessary API calls

Batch Requests

Combine multiple requests when the API supports it

Add Delays

Include small delays between requests to avoid bursts

Handle Gracefully

Implement proper retry logic with backoff

Don't Hammer

Avoid rapid successive requests to the same endpoint

Don't Ignore Limits

Respect rate limit errors and wait before retrying