Most "free tier" API posts assume a generous budget. This one is about the
other case: an API that allows three requests per hour, per endpoint. I have
been building a small crypto dashboard against one of those, and the limit
changed how I write client code more than any performance trick ever has.
The numbers below come from The Coin Analysis public price API, which I picked
because its free tier is exactly that strict. The ideas apply to any API where
calls are the scarce resource.
One wide call beats twenty narrow ones
The instinct is to request one token, then the next, then the next. Three calls
in, you are locked out for the hour.
Almost every list endpoint accepts a page size, and it is usually larger than
you think. Here the whole universe comes back in a single request:
curl "https://www.thecoinanalysis.com/api/public/v1/prices?perPage=250&order=market_cap" \
-H "x-api-key: $KEY"
That returned 220 tokens and "pages": 1 in the meta block. One call, the
entire dataset, and filtering afterwards costs nothing. If you only care about
a handful, ask for them by name rather than one at a time:
curl ".../prices?symbols=BTC,ETH,SOL" -H "x-api-key: $KEY"
Three tokens, one call. The rule is simple: every loop that contains an HTTP
request is a bug waiting for a 429.
Refresh on a timer
Three calls per hour leaves one refresh every twenty minutes. Refreshing when a
user asks means the third visitor of the hour gets an error page.
So the fetch belongs on a timer, and the request path only ever reads the last
good value:
let cache = { at: 0, data: null };
const WINDOW = 20 * 60 * 1000;
async function prices() {
if (Date.now() - cache.at < WINDOW && cache.data) return cache.data;
const r = await fetch(`${BASE}/prices?perPage=250`, {
headers: { "x-api-key": process.env.KEY },
});
if (r.status === 429) return cache.data; // serve stale, never throw
cache = { at: Date.now(), data: await r.json() };
return cache.data;
}
The important line is the 429 branch. Stale prices are a small inconvenience.
An exception in a request handler is a broken page.
The server already did the maths
The history endpoint returns the series and the derived numbers together:
{ "history": [ { "ts": "2026-08-16T15:00:00.000Z", "price": 63044 } ],
"volatility": 0.5958, "maxDrawdown": 39.0, "athUsd": 126080 }
Volatility and maximum drawdown for that window, already computed. Pulling raw
candles to calculate them yourself costs the same single call and adds code you
now have to test. When a rate limit is tight, prefer the endpoint that answers
the question over the one that returns the ingredients.
Where the 429 actually applies
This one cost me an afternoon. When the list endpoint was exhausted, I backed
off everything. But the window is counted per endpoint, so detail and history
were still available the whole time.
Track the reset per route:
const blocked = new Map(); // route -> timestamp
async function call(route) {
if (Date.now() < (blocked.get(route) ?? 0)) throw new Error("cooling down");
const r = await fetch(BASE + route, { headers: { "x-api-key": KEY } });
if (r.status === 429) {
blocked.set(route, Date.now() + 60 * 60 * 1000);
throw new Error("rate limited");
}
return r.json();
}
Blanket backoff throws away quota you still have.
About that wildcard CORS header
The API answers with access-control-allow-origin: *, so the browser will
happily call it. That does not mean you should: doing it from the front end
ships your key to everyone who opens the devtools, and a shared key burns three
requests in one page load.
Keep the key on a server or an edge function, cache there, and let the browser
read your own endpoint. With a single call feeding every visitor for twenty
minutes, the strict tier stops mattering.
What I would keep
A tight free tier turned out to be a good teacher. Batching instead of looping,
caching on a timer, preferring the computed answer to the raw series, scoping
the backoff to one route: none of that is specific to a stingy quota. It makes a
generous API better too, which is the part I did not expect.
The endpoints and the free key are documented at
The Coin Analysis if you want to
try the same exercise.
Top comments (0)