How not to burn your free tier

8 September 2026 · 6 min read

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:

Endpointcache-controlChanges when
Live scoresmax-age=15A goal goes in
Fixtures, results, standingsmax-age=300A match finishes
Squads, players, team profilesmax-age=86400Somebody 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.

More

Head-to-head records, and what they are actually good for

They settle ties in three leagues and fill every match preview. Tested as a predictor on 3,042 league matches, this season's form beat them.

Why we serve no table rather than half a season

A league table built from part of a season is not partial data, it is wrong data that looks right. The rule we apply, what it costs us, and why a 404 is the better answer.

Two teams, same points: every league breaks the tie differently

Goal difference decides it in England, Germany and France. In Spain, Italy, Portugal and Turkey the head-to-head record comes first. Sort a table yourself and you have it wrong for four of the twelve.

Build a live football scoreboard

A working scoreboard in about 60 lines: what /matches/live returns, why you poll every 30 seconds and not faster, and why your API key must never reach the browser.

What's inside a match: lineups, events and ratings

A field-by-field walk through a match detail response: formations, starting elevens, the event timeline, and how to rebuild a goal-by-goal summary from it.

Try it

Every endpoint mentioned here is in the reference, and every listing has a free tier. Start on the front page.