Guides/Caching Best Practices

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 TypeCache DurationNotes
Player career stats5-15 minutesUpdates infrequently
Match history5 minutesAppend new matches only
Tournament info1 hourRarely changes
Leaderboards1-5 minutesDepends on refresh rate needs
Live matches5-30 secondsOr 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.