Caching Best Practices
Optimize performance and reduce API calls
Caching is essential for esports applications. It reduces API calls, improves response times, and helps you stay within rate limits.
90%
Fewer API calls with proper caching
<50ms
Cache hit response time
10x
More effective rate limit usage
What to Cache
| Data Type | Cache Duration | Notes |
|---|---|---|
| Player career stats | 5-15 minutes | Updates infrequently |
| Match history | 5 minutes | Append new matches only |
| Tournament info | 1 hour | Rarely changes |
| Leaderboards | 1-5 minutes | Depends on refresh rate needs |
| Live matches | 5-30 seconds | Or use webhooks instead |
In-Memory Caching
cache.ts
import { LRUCache } from 'lru-cache';
const cache = new LRUCache<string, any>({
max: 500, // Maximum items
ttl: 1000 * 60 * 5, // 5 minute default TTL
});
async function cachedFetch<T>(
key: string,
fetcher: () => Promise<T>,
ttl?: number
): Promise<T> {
const cached = cache.get(key);
if (cached) return cached as T;
const data = await fetcher();
cache.set(key, data, { ttl });
return data;
}
// Usage
export async function getPlayer(username: string) {
return cachedFetch(
`player:${username}`,
() => cito.fortnite.players.get(username),
1000 * 60 * 10 // 10 minutes
);
}Redis Caching (Distributed)
For multi-server deployments, use Redis:
redis-cache.ts
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
async function cachedFetch<T>(
key: string,
fetcher: () => Promise<T>,
ttlSeconds: number = 300
): Promise<T> {
const cached = await redis.get(key);
if (cached) return JSON.parse(cached);
const data = await fetcher();
await redis.setex(key, ttlSeconds, JSON.stringify(data));
return data;
}
// Invalidate cache when data changes
async function invalidatePlayerCache(username: string) {
const keys = await redis.keys(`player:${username}:*`);
if (keys.length) await redis.del(...keys);
}
// Usage with webhook
app.post('/webhook', async (req, res) => {
const { event, data } = req.body;
if (event === 'match.ended') {
// Invalidate player caches for participants
for (const player of data.players) {
await invalidatePlayerCache(player.username);
}
}
res.sendStatus(200);
});Client-Side Caching with React Query
import { QueryClient, QueryClientProvider, useQuery } from '@tanstack/react-query';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 1000 * 60 * 5, // 5 minutes
cacheTime: 1000 * 60 * 30, // 30 minutes
refetchOnWindowFocus: false,
},
},
});
function PlayerStats({ username }) {
const { data, isLoading, isStale } = useQuery({
queryKey: ['player', username],
queryFn: () => fetch(`/api/player/${username}`).then(r => r.json()),
staleTime: 1000 * 60 * 10, // 10 minutes for player stats
});
return (
<div>
{isStale && <Badge>Refreshing...</Badge>}
{data && <StatsDisplay stats={data} />}
</div>
);
}
// Prefetch on hover
function PlayerLink({ username }) {
const prefetch = () => {
queryClient.prefetchQuery({
queryKey: ['player', username],
queryFn: () => fetch(`/api/player/${username}`).then(r => r.json()),
});
};
return (
<Link href={`/player/${username}`} onMouseEnter={prefetch}>
{username}
</Link>
);
}Cache Invalidation Strategies
Time-based expiration (TTL)
Simplest approach. Set appropriate TTLs based on data freshness needs.
Event-based invalidation
Use webhooks to invalidate cache when data changes.
Stale-while-revalidate
Serve stale data immediately while fetching fresh data in background.