Maximizing Efficiency: Best Practices for Handling Rate Limits with the Realtime Sports API
Maximizing Efficiency: Best Practices for Handling Rate Limits with the Realtime Sports API
The Realtime Sports API has two separate limits. Most "rate limit" problems come from treating them as one:
- A monthly quota. This is the number of calls your plan includes per calendar month.
- A per-second rate. This caps how fast you can send requests.
They fail in different ways, so they need different handling. This guide covers both, shows exactly what the API returns, and explains how to get the most out of your plan.
The two limits
| Plan | Price | Monthly quota | Per-second rate |
|---|---|---|---|
| Free | $0 | 125 calls (1,000 in your first 30 days) | 1/s |
| Starter | $29/mo | 10,000 | 5/s |
| Growth | $49/mo | 25,000 | 10/s |
| Pro | $99/mo | 50,000 | 20/s |
| Scale | $299/mo | 500,000 | 100/s |
On Starter and Growth, REST calls, WebSocket messages and webhook deliveries all draw from one shared monthly pool. Paid monthly plans (not annual) can opt in to overage at $2 per 1,000 extra units, up to a monthly dollar cap you set (the default cap is $25; a cap of $0 means a hard stop). Without overage, requests stop at the quota until the next month starts. See pricing for the current details.
Reading your quota: meta.rateLimit
Successful responses include your quota status in the response body, under meta.rateLimit:
{
"success": true,
"data": [ ... ],
"meta": {
"rateLimit": {
"limit": 10000,
"remaining": 8421,
"reset": 1793491199999
}
}
}
limitis your plan's monthly quota (10,000 here, on Starter).remainingis how many calls you have left this month.resetis a Unix timestamp in milliseconds for when the monthly quota resets. It is not a number of seconds to wait.
The quota is reported in the body, not in X-RateLimit-* response headers. Your usage dashboard shows the same numbers. A simple habit: log remaining and alert yourself when it falls below 10–20% of limit.
What a 429 means, and what to do about it
Both limits return HTTP 429. Check error.code to tell them apart.
Per-second limit: RATE_LIMIT_PER_SECOND. You sent requests faster than your plan allows. The response includes a Retry-After header in seconds (usually 1) and an X-RateLimit-Limit-Second header with your rate. These rejected requests do not count against your monthly quota. Wait for Retry-After and try again. Better still, space your requests out so this never happens.
Monthly quota: RATE_LIMIT_EXCEEDED. You have used this month's allowance, and overage is off or has reached its cap. Retrying won't help. The Retry-After header here can be hours or days, because it counts down to the monthly reset. Stop polling, show the user something sensible, and either turn on overage or upgrade.
Retry with exponential backoff only for network errors and 5xx responses. Using it on a monthly-quota 429 just burns through requests that will all be rejected.
const BASE = 'https://www.realtimesportsapi.com/api/v1';
async function callApi(path, apiKey, { maxRetries = 3 } = {}) {
for (let attempt = 0; ; attempt++) {
let res;
try {
res = await fetch(`${BASE}${path}`, { headers: { Authorization: `Bearer ${apiKey}` } });
} catch (networkError) {
if (attempt >= maxRetries) throw networkError;
await sleep(2 ** attempt * 1000); // network hiccup: back off and retry
continue;
}
if (res.status === 429) {
const body = await res.json();
const retryAfterSec = Number(res.headers.get('Retry-After') || 1);
if (body.error?.code === 'RATE_LIMIT_PER_SECOND' && attempt < maxRetries) {
await sleep(retryAfterSec * 1000); // too fast: wait as told, then retry
continue;
}
// RATE_LIMIT_EXCEEDED: monthly quota used up. Don't retry; surface it.
throw Object.assign(new Error(body.error?.message || 'Quota exceeded'), { code: body.error?.code, retryAfterSec });
}
if (res.status >= 500 && attempt < maxRetries) {
await sleep(2 ** attempt * 1000);
continue;
}
const body = await res.json();
if (!body.success) throw Object.assign(new Error(body.error?.message), { code: body.error?.code });
return body; // body.meta.rateLimit has limit / remaining / reset
}
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
Best practices for staying inside your quota
1. Do the polling math before you ship
Live data is typically 20–30 seconds behind live play (that is how far the upstream source runs behind the field). Polling more often than every 15–30 seconds gains you almost nothing. Each poll is one call, so the cost adds up fast. One live game polled every 10 seconds for 3 hours is about 1,080 calls, which is more than the Free tier's whole month.
2. Ask for lists, not items
One events/live call returns every live game in a league:
curl "https://www.realtimesportsapi.com/api/v1/sports/basketball/leagues/nba/events/live" \
-H "Authorization: Bearer YOUR_API_KEY"
That beats polling each game's detail endpoint separately. Fetch per-game data such as plays or boxscore only for the games your user is actually viewing.
3. Cache what doesn't change
Teams, rosters, season calendars and finished games rarely change. Cache them for hours, or until the next day. For live scores, a cache of 15–30 seconds shared by all your users means one API call serves everyone who is watching that game.
4. Let us push instead of you polling
If you only need to know when something happens, use push delivery. Then you aren't asking "anything new?" every few seconds.
- Webhooks (all paid plans) send a signed POST to your endpoint for
event.live,event.score_change,event.status_change,event.play,event.finalandevent.odds_change. Each delivery counts as one unit. See Leveraging Webhooks for Instant Sports Data Updates. - WebSocket streams the same updates over a single connection. Free accounts get a 500-message monthly preview.
Push delivery has the same 20–30 second upstream delay. What it saves is the calls spent polling when nothing has changed.
5. Coverage matters for budgeting
Live data, WebSocket and webhooks cover the NFL, college football, the NBA, men's college basketball, MLB, the NHL and 23 soccer competitions: 29 leagues in all. Don't poll events/live for leagues outside that list and expect live updates.
Summary
- Two limits: a monthly quota, read from
meta.rateLimit(resetis epoch milliseconds), and a per-second rate. 429+RATE_LIMIT_PER_SECOND: wait forRetry-After, then retry.429+RATE_LIMIT_EXCEEDED: stop, then enable overage or upgrade.- Back off exponentially only on network errors and
5xx. - Poll no faster than the data changes (every 15–30 s at most), cache shared and static data, and use webhooks or WebSocket for live updates.
There is no uptime SLA. Build for the occasional failed request: handle errors gracefully and keep showing the last data you received rather than a blank screen.