Skip to content

Caching

The iRacing Data Client SDK supports opt-in caching via pluggable stores powered by @http-client-toolkit/core. When a CacheStore is provided, resolved GET results are cached automatically. For iRacing’s usual presigned S3 responses, TTL is derived from the presigned URL’s expiry.

Pass a CacheStore implementation via the stores option:

import { IRacingDataClient } from 'iracing-data-client';
const iracing = new IRacingDataClient({
auth: { /* ... */ },
stores: {
cache: myCacheStore,
},
});
// First call — network request, response is cached
const cars1 = await iracing.car.get();
// Second call within cache period — returned from store
const cars2 = await iracing.car.get();

When caching is enabled:

  1. S3 Link Resolution — The SDK follows presigned S3 URLs returned by the iRacing API
  2. TTL Derivation — S3-link responses get Cache-Control: max-age from the API expires field; direct responses use normal HTTP cache headers, or the toolkit fallback TTL when no cache headers are present
  3. Cache Entry — The toolkit stores the parsed, transformed SDK result together with cache metadata
  4. Cache Key — Generated from the full request URL including query parameters
  5. Transparent — Cached results are returned automatically; no code changes needed per-call

Cache keys are derived from the request URL and parameters, so different parameters produce different cache entries:

await iracing.member.get({ custIds: [123456] }); // Cache key 1
await iracing.member.get({ custIds: [789012] }); // Cache key 2
await iracing.member.get({ custIds: [123456] }); // Hits cache key 1

The SDK does not hard-code per-endpoint cache durations. Cache lifetime is determined by the response metadata that reaches @http-client-toolkit/core:

S3 Link Responses

TTL is calculated from the iRacing API’s presigned URL expires field

Direct HTTP Responses

Standard HTTP cache headers such as Cache-Control, Expires, and Last-Modified are respected

No Cache Headers

The toolkit uses its fallback TTL for cacheable responses. In @http-client-toolkit/core@4.2.0, that fallback is 3600 seconds.

If you need endpoint-specific freshness rules, layer an application cache on top of the SDK or implement them inside your chosen store.

Install a store package separately. Published toolkit stores include @http-client-toolkit/store-memory, @http-client-toolkit/store-sqlite, and @http-client-toolkit/store-dynamodb.

import { IRacingDataClient } from 'iracing-data-client';
import { InMemoryCacheStore } from '@http-client-toolkit/store-memory';
const iracing = new IRacingDataClient({
auth: { /* ... */ },
stores: {
cache: new InMemoryCacheStore({
maxItems: 1000,
maxMemoryBytes: 50_000_000,
}),
},
});

See the http-client-toolkit repository for the latest store package list and options.

Prefer a published store package where possible. If you implement your own, use the CacheStore contract from @http-client-toolkit/core. The value is toolkit-managed cache data, not a raw Response, so store and return it unchanged.

interface CacheStore<T = unknown> {
get(hash: string): Promise<T | undefined>;
set(hash: string, value: T, ttlSeconds: number): Promise<void>;
delete(hash: string): Promise<void>;
clear(scope?: string): Promise<void>;
setWithTags(
hash: string,
value: T,
ttlSeconds: number,
tags: string[]
): Promise<void>;
invalidateByTag(tag: string): Promise<number>;
invalidateByTags(tags: string[]): Promise<number>;
}

Simple in-memory example:

import type { CacheStore } from '@http-client-toolkit/core';
class SimpleCacheStore<T = unknown> implements CacheStore<T> {
private entries = new Map<string, {
value: T;
expiresAt: number;
tags: Set<string>;
}>();
private tagIndex = new Map<string, Set<string>>();
async get(hash: string): Promise<T | undefined> {
const entry = this.entries.get(hash);
if (!entry) return undefined;
if (entry.expiresAt <= Date.now()) {
await this.delete(hash);
return undefined;
}
return entry.value;
}
async set(hash: string, value: T, ttlSeconds: number): Promise<void> {
await this.setWithTags(hash, value, ttlSeconds, []);
}
async setWithTags(
hash: string,
value: T,
ttlSeconds: number,
tags: string[]
): Promise<void> {
await this.delete(hash);
const tagSet = new Set(tags);
this.entries.set(hash, {
value,
expiresAt: Date.now() + ttlSeconds * 1000,
tags: tagSet,
});
for (const tag of tagSet) {
const hashes = this.tagIndex.get(tag) ?? new Set<string>();
hashes.add(hash);
this.tagIndex.set(tag, hashes);
}
}
async delete(hash: string): Promise<void> {
const entry = this.entries.get(hash);
if (!entry) return;
this.entries.delete(hash);
for (const tag of entry.tags) {
const hashes = this.tagIndex.get(tag);
hashes?.delete(hash);
if (hashes?.size === 0) {
this.tagIndex.delete(tag);
}
}
}
async clear(scope?: string): Promise<void> {
for (const hash of [...this.entries.keys()]) {
if (!scope || hash.startsWith(scope)) {
await this.delete(hash);
}
}
}
async invalidateByTag(tag: string): Promise<number> {
const hashes = [...(this.tagIndex.get(tag) ?? [])];
await Promise.all(hashes.map((hash) => this.delete(hash)));
return hashes.length;
}
async invalidateByTags(tags: string[]): Promise<number> {
const hashes = new Set<string>();
for (const tag of tags) {
for (const hash of this.tagIndex.get(tag) ?? []) {
hashes.add(hash);
}
}
await Promise.all([...hashes].map((hash) => this.delete(hash)));
return hashes.size;
}
}
const iracing = new IRacingDataClient({
auth: { /* ... */ },
stores: {
cache: new SimpleCacheStore(),
},
});

If you need caching logic beyond what the store provides (e.g. custom TTLs per endpoint, conditional caching), you can layer your own cache on top:

class CachedIRacingService {
private cache = new Map<string, { data: unknown; expires: number }>();
constructor(private iracing: IRacingDataClient) {}
async getMemberInfo(
custId: number,
cacheDuration = 5 * 60 * 1000 // 5 minutes
) {
const cacheKey = `member:${custId}`;
const cached = this.cache.get(cacheKey);
if (cached && cached.expires > Date.now()) {
return cached.data;
}
const data = await this.iracing.member.get({ custIds: [custId] });
this.cache.set(cacheKey, {
data,
expires: Date.now() + cacheDuration,
});
return data;
}
clearCache() {
this.cache.clear();
}
}

Warm up the cache on application start:

async function preloadCache(iracing: IRacingDataClient) {
await Promise.all([
iracing.car.get(),
iracing.track.get(),
iracing.series.get(),
iracing.constants.categories(),
iracing.constants.divisions(),
]);
}
await preloadCache(iracing);

Combine requests to reduce cache entries and API calls:

// Instead of fetching members one at a time
const ids = [123, 456, 789];
// Single batched request
const members = await iracing.member.get({ custIds: ids });

Use Pluggable Stores

Pass a CacheStore via stores.cache for automatic, TTL-aware caching with no extra code

Cache Static Data

Aggressively cache data that rarely changes (cars, tracks, series)

Monitor Performance

Track cache hit rates to validate your caching strategy

Don't Over-Cache

Avoid caching real-time data that needs to be fresh