Guides / Caching

Caching

Most sports data changes far less often than it is read. A small cache in front of the API makes your app faster and keeps you well inside your limits.

Samples use the small get() helper from Build a live score app.

Why cache

Without a cache, every visitor triggers an API request of their own. With one, a thousand people watching the same match cost you one request a minute.

  • Fewer requests — stay well inside your monthly quota and rate limit.
  • Faster pages — responses come from memory instead of a network round trip.
  • Resilience — you can keep serving the last good response if a request fails.

What to cache and for how long

Match the lifetime of each cache entry to how the data changes. Live data is about 30 seconds fresh at source, so caching it for the 60-second polling interval costs your users very little.

Suggested cache lifetimes
DataExampleCache for
Live scores/football/fixtures/live60 seconds — the recommended polling interval.
Today’s fixtures/football/fixtures?date=…Until the next kick-off; then follow the live endpoint.
Upcoming fixtures/football/fixtures?date=…Refresh on a schedule — kick-off times can move.
Standings/football/standingsUntil a fixture in that league finishes.
Finished fixtures, past seasons/football/fixtures?season=…A long time — final results rarely change.
Leagues, teams, players/football/teamsA long time; refresh on a slow schedule.

A shared server-side cache

Put the cache on your server, in front of every API call, so all your users share it. An in-memory cache is enough for one server; use Redis or your CDN once you run several.

const cache = new Map();
function cached(key, ttlMs, load) {
const hit = cache.get(key);
if (hit && hit.expires > Date.now()) return hit.promise;
// Store the promise, so concurrent callers share one request
const promise = load().catch((err) => {
cache.delete(key);
throw err;
});
cache.set(key, { promise, expires: Date.now() + ttlMs });
return promise;
}
// Every visitor in the same minute shares one API request
const live = await cached("football:live:39", 60 * 1000, () =>
get("/football/fixtures/live", { league_id: 39 }),
);

In a multi-threaded Python server, guard the dictionary with a lock or use a library such as cachetools.

Invalidating on events

Time-based expiry is a safety net. When data changes on a known event, drop the entry as soon as the event happens:

  • A fixture reaches FT → its league’s standings and that day’s fixtures.
  • A new season starts → leagues and teams.
// Call this when a fixture reaches FT (see "Handling live matches")
function onFinished(fixture) {
cache.delete("football:standings:" + fixture.league.id);
cache.delete("football:fixtures:" + fixture.date.slice(0, 10));
}

Detecting FT is covered in Handling live matches.

Caching for your own clients

If your backend serves JSON to browsers or apps, a Cache-Control header lets your CDN share one response between users too. s-maxage applies to shared caches such as a CDN; stale-while-revalidate lets them serve the previous response while fetching a fresh one.

// Your route handler: let your CDN share the response for 60 seconds
return Response.json(live, {
headers: { "Cache-Control": "public, s-maxage=60, stale-while-revalidate=30" },
});

Cache on your side of the proxy

Browsers and apps should call your backend, never the API directly — your key must stay on the server. See Call the API from a server.