How not to burn your free tier
A free tier runs out for one of two reasons. Either you have real traffic, which is a good problem, or you are asking for data that has not changed since the last time you asked. Almost always it is the second one.
Three lifetimes, and they are on every response
Every response tells you how long it is good for. You do not have to guess:
| Endpoint | cache-control | Changes when |
|---|---|---|
| Live scores | max-age=15 | A goal goes in |
| Fixtures, results, standings | max-age=300 | A match finishes |
| Squads, players, team profiles | max-age=86400 | Somebody transfers |
That last row is where the quota goes. A squad list is good for a day. If your app fetches it on every page load, you are spending hundreds of calls to receive the same 28 players.
The fix is about ten lines
Browsers honour these headers for free. On a server you have to opt in:
const cache = new Map();
async function get(path) {
const hit = cache.get(path);
if (hit && hit.until > Date.now()) return hit.body;
const res = await fetch(BASE + path, { headers: HEADERS });
const body = await res.json();
const found = /max-age=(\d+)/.exec(res.headers.get('cache-control') || '');
const seconds = found ? Number(found[1]) : 60;
cache.set(path, { body, until: Date.now() + seconds * 1000 });
return body;
}
It reads the lifetime off the response rather than hard-coding a number, so it stays correct if the number ever changes.
Polling faster than the data moves
Our updater reads the source every 30 seconds during a match. Poll us once a second and you get the same answer thirty times, then a new one. Thirty calls to be, on average, fifteen seconds fresher.
Match data is not a stock price. Nobody notices fifteen seconds. Everybody notices a quota that ran out on a Saturday afternoon.
Ask for less
The match detail response is the largest in the API, because it carries two full squads with a rating and a stat line per player plus the event timeline. The match list carries the score.
Rendering a results grid? Use /matches, and fetch the detail when somebody clicks.
For most apps this is the single largest reduction available, and the grid gets faster too.
Same with pagination. limit defaults to 50. If you are showing ten, ask for ten.
One key, one fetcher
If your key is in browser JavaScript, your quota is multiplied by everyone with the page open, and your key is public. One server-side fetcher behind that cache turns a thousand readers into one call. It is the same proxy as in the scoreboard article, and it pays twice: once in quota, once in not having your key scraped.
ETags, with a caveat
Responses carry an ETag. Send it back as If-None-Match and an unchanged
resource answers 304 Not Modified with no body, which saves bandwidth.
It does not save quota. A 304 is still a request and the marketplace still counts it. Useful on a slow connection, useless as a quota strategy. Cache on lifetime instead.
What good looks like
A scoreboard for one league, polling live scores every 30 seconds but only while matches are actually being played, is 720 calls on a six-hour Saturday. Add squads and standings once a day and a month of that lands somewhere around 3,000.
Worth saying plainly: that does not fit the free tier. 500 requests a month is enough to build against, and enough to run a page that refreshes a table a few times a day. It is not enough to poll live scores. A single-league scoreboard belongs on the $5 plan at 5,000 a month, and the tenfold steps above it exist because polling a second and third competition multiplies from there.
What the caching above buys you is that none of this scales with your audience. Ten thousand readers and one reader cost exactly the same, because they are all reading from your cache rather than from us.