Integration · 12 min read

Caching Strategies for Sports API Data

Learn how to cache live scores, fixtures, standings, and historical data to reduce API costs, respect rate limits, and deliver fast responses to your users.

Last updated: August 2026

Overview

Sports data has a unique characteristic: some of it changes every second (live scores), some changes daily (standings, fixtures), and some never changes at all (historical match results from last season). A one-size-fits-all cache TTL wastes either money or freshness. The key is tiered caching with data-type-specific expiration strategies.

A well-designed caching layer can reduce API calls by 80-95%, which directly translates to lower costs and higher rate limit headroom. For a typical live score app serving 10,000 users, effective caching can be the difference between a $50/month plan and a $500/month plan.

Pro tip: Use our cost calculator to estimate how much you can save by implementing caching for your specific API and usage pattern.

Architecture

A tiered caching architecture assigns different TTLs based on data volatility:

Data TypeVolatilityRecommended TTL
Live scores (in-progress)Seconds5-15 seconds
Fixtures (today)Minutes60 seconds
StandingsHours1-6 hours
Fixtures (future)Hours6-12 hours
Historical resultsNeverPermanent (30+ days)
Team/league metadataRarely24 hours
OddsSeconds to minutes30-60 seconds

Implementation

Redis-Based Cache Layer (JavaScript)

Here is a cache wrapper that applies different TTLs based on the endpoint type. It uses Redis for distributed caching across multiple server instances:

// cache.js
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL);

// TTL mapping by data type (in seconds)
const TTL_STRATEGY = {
  live: 10,          // Live scores: 10 seconds
  fixtures_today: 60, // Today's fixtures: 1 minute
  fixtures_future: 21600, // Future fixtures: 6 hours
  standings: 3600,   // Standings: 1 hour
  historical: 2592000, // Historical: 30 days
  metadata: 86400,   // Team/league info: 24 hours
  odds: 30,          // Odds: 30 seconds
};

function categorizeEndpoint(endpoint) {
  if (endpoint.includes("live=")) return "live";
  if (endpoint.includes("odds")) return "odds";
  if (endpoint.includes("standings")) return "standings";
  if (endpoint.includes("lineups") || endpoint.includes("statistics"))
    return "live";
  if (endpoint.includes("fixtures")) {
    // Check if it's historical or future
    if (endpoint.match(/date=20[12][0-9]/)) return "historical";
    return "fixtures_future";
  }
  if (endpoint.includes("teams") || endpoint.includes("leagues"))
    return "metadata";
  return "fixtures_today"; // Default
}

export async function cachedFetch(url, apiKey) {
  const category = categorizeEndpoint(url);
  const ttl = TTL_STRATEGY[category];
  const cacheKey = `api:${Buffer.from(url).toString("base64")}`;

  // Try cache first
  const cached = await redis.get(cacheKey);
  if (cached) {
    return {
      data: JSON.parse(cached),
      source: "cache",
      ttl,
    };
  }

  // Cache miss — fetch from API
  const response = await fetch(url, {
    headers: { "x-apisports-key": apiKey },
  });

  if (!response.ok) {
    throw new Error(`API error: ${response.status}`);
  }

  const data = await response.json();

  // Store in cache with appropriate TTL
  await redis.setex(cacheKey, ttl, JSON.stringify(data));

  return {
    data,
    source: "api",
    ttl,
  };
}

Stale-While-Revalidate Pattern (Python)

The stale-while-revalidate (SWR) pattern serves stale data immediately while fetching fresh data in the background. This gives users instant responses while keeping data reasonably fresh:

# swr_cache.py
import json
import time
import asyncio
import httpx
import redis

r = redis.Redis.from_url("redis://localhost:6379")

# Store both the data and its "stale" timestamp
# stale_after < ttl, so we serve stale data while refreshing
CACHE_CONFIG = {
    "live": {"ttl": 30, "stale_after": 10},
    "standings": {"ttl": 7200, "stale_after": 3600},
    "fixtures": {"ttl": 3600, "stale_after": 300},
    "default": {"ttl": 60, "stale_after": 30},
}

def get_config(endpoint):
    if "live=" in endpoint:
        return CACHE_CONFIG["live"]
    if "standings" in endpoint:
        return CACHE_CONFIG["standings"]
    if "fixtures" in endpoint:
        return CACHE_CONFIG["fixtures"]
    return CACHE_CONFIG["default"]

async def swr_fetch(url, api_key, background_tasks=None):
    config = get_config(url)
    cache_key = f"swr:{url}"
    raw = r.get(cache_key)

    if raw:
        entry = json.loads(raw)
        age = time.time() - entry["fetched_at"]

        if age < config["stale_after"]:
            # Fresh — return immediately
            return {"data": entry["data"], "source": "cache-fresh"}

        if age < config["ttl"]:
            # Stale but usable — return and refresh in background
            if background_tasks:
                background_tasks.add_task(
                    refresh_cache, url, api_key, cache_key, config
                )
            return {"data": entry["data"], "source": "cache-stale"}

    # No cache or expired — fetch synchronously
    data = await fetch_from_api(url, api_key)
    r.setex(cache_key, config["ttl"], json.dumps({
        "data": data,
        "fetched_at": time.time(),
    }))
    return {"data": data, "source": "api"}

async def fetch_from_api(url, api_key):
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            url,
            headers={"x-apisports-key": api_key},
            timeout=10,
        )
        resp.raise_for_status()
        return resp.json()

async def refresh_cache(url, api_key, cache_key, config):
    """Background task to refresh stale cache entries."""
    try:
        data = await fetch_from_api(url, api_key)
        r.setex(cache_key, config["ttl"], json.dumps({
            "data": data,
            "fetched_at": time.time(),
        }))
        print(f"Refreshed cache for {url}")
    except Exception as e:
        print(f"Background refresh failed for {url}: {e}")

Code Examples

Next.js Route Handler with Caching

In a Next.js App Router application, you can combine the native revalidate option with your own cache layer for maximum efficiency:

// app/api/scores/route.js
import { NextResponse } from "next/server";
import { cachedFetch } from "@/lib/cache";

export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const league = searchParams.get("league") || "39"; // Premier League
  const season = searchParams.get("season") || "2025";

  const url = `https://v3.football.api-sports.io/fixtures?live=all&league=${league}&season=${season}`;

  try {
    const result = await cachedFetch(url, process.env.SPORTS_API_KEY);

    return NextResponse.json(
      {
        data: result.data,
        source: result.source,
        cacheTtl: result.ttl,
      },
      {
        headers: {
          // Let the browser cache for a few seconds too
          "Cache-Control": "public, s-maxage=10, stale-while-revalidate=30",
        },
      }
    );
  } catch (error) {
    return NextResponse.json(
      { error: error.message },
      { status: 503 }
    );
  }
}

Best Practices

Never cache without a TTL

Every cache entry must have an expiration. Even historical data should eventually be revalidated in case the provider corrects an error. Use a 30-day TTL as a maximum ceiling.

Use cache invalidation for critical updates

When a match starts or ends, proactively invalidate the cache for related endpoints (fixtures, standings, odds). This ensures users see fresh data immediately rather than waiting for the TTL to expire.

Add a stale indicator to responses

Include a source field in your API responses so clients know whether data came from cache or the upstream API. This helps with debugging and lets the UI show a freshness indicator.

Monitor cache hit ratio

Track your cache hit ratio over time. A healthy ratio is 85% or higher. If it drops, your TTLs may be too short or your cache keys may be too granular (including unnecessary query parameters).

Related Guides

Calculate your caching savings

Estimate how much you can save with smart caching strategies.