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.
| Data | Example | Cache for |
|---|---|---|
| Live scores | /football/fixtures/live | 60 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/standings | Until a fixture in that league finishes. |
| Finished fixtures, past seasons | /football/fixtures?season=… | A long time — final results rarely change. |
| Leagues, teams, players | /football/teams | A 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 requestconst 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 secondsreturn 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.