DEV Community

Cover image for Why Your Live Scores Are Always 30 Seconds Late (and How to Fix It)
orbistats
orbistats

Posted on

Why Your Live Scores Are Always 30 Seconds Late (and How to Fix It)

If you've ever built a live-score feature and had a user say "the score changed on ESPN's app before yours updated," you've hit the most common architecture mistake in sports data integration: polling REST endpoints for something that should be pushed, not pulled.

Let's break down exactly where the delay comes from, and the code that fixes it.

First: how much delay are we actually talking about?

This isn't a vague complaint — it's measurable, and it's bigger than most people assume. Broadcast latency research on live sports specifically found an order-of-magnitude discrepancy in delay, accuracy, and data diversity among different data-feed providers when measuring the same NBA and EPL games across 40 sports websites. Separately, industry coverage of OTT video delivery has documented that live sports streaming can carry over a minute of latency, quite often landing between 20 and 40 seconds — and general streaming-latency reporting puts typical cord-cutting delay in the 15-to-60-second range depending on the pipeline.

So "30 seconds late" isn't an exaggeration for a bad integration — it's roughly the middle of the normal range for anything built on the slow path.

The slow path: REST polling

Here's the pattern almost everyone starts with, because it's the first thing that works:

javascript
// The naive approach — works, but is structurally slow
async function pollScore(fixtureId) {
setInterval(async () => {
const res = await fetch(
https://api.example.com/v1/football/fixtures/${fixtureId},
{ headers: { Authorization: Bearer ${API_KEY} } }
);
const data = await res.json();
updateUI(data.score);
}, 15000); // poll every 15 seconds
}

This looks reasonable until you count where every second of delay actually comes from:

Provider-side collection delay — however the provider gets its data (broadcast feed, venue sensor, human spotter), there's inherent lag before it even reaches their system.
Provider-side processing/batching delay — many providers batch updates rather than pushing them instantly.
Your polling interval — if you poll every 15 seconds, your average staleness is 7.5 seconds even in the best case, and up to 15 seconds in the worst case, on top of everything above.
Network round-trip per poll — each request pays connection + TLS handshake overhead again, especially over plain HTTP/1.1 keep-alive misconfigurations.

Layer all four, and you land exactly in that 20-40 second range documented above — before you've done anything "wrong" from a code-quality perspective. The architecture itself is the bottleneck.

A direct comparison from an odds-latency provider frames this well: they explicitly label the REST-polling approach "The Slow Way," contrasting it against a persistent WebSocket connection — because polling means you connect only when you decide to ask, not when the event actually happens.

Why polling faster doesn't really fix it

The obvious instinct is to just poll more often:

javascript
// Tempting, but this doesn't solve the actual problem
setInterval(pollScore, 2000); // every 2 seconds now

This helps marginally, but creates new problems:

Your rate limit burns 7.5x faster for the same coverage window.
You're now making mostly wasted requests — most 2-second windows have no change at all.
You still have an average delay equal to half your polling interval, plus all the provider-side delay you can't control from your side.

You've traded "clearly too slow" for "expensive and still not real-time."

The fast path: WebSocket push

Instead of asking "did anything change?" repeatedly, you open one persistent connection and get told the moment something does:

javascript
const ws = new WebSocket("wss://stream.orbistats.com/v1/live");

ws.onopen = () => {
ws.send(JSON.stringify({
action: "subscribe",
channel: "live_scores",
fixture_id: "fixture_50231"
}));
};

ws.onmessage = (event) => {
const update = JSON.parse(event.data);
// fires the instant the score changes — no polling delay, no wasted requests
updateUI(update.score);
};

ws.onerror = (err) => console.error("WebSocket error:", err);

ws.onclose = () => {
// reconnect logic — don't let a dropped connection silently stop updates
setTimeout(() => connectLiveScores(fixtureId), 1000);
};

Compare the two models directly:

REST polling    WebSocket push
Enter fullscreen mode Exit fullscreen mode

Delay source provider lag + your poll interval provider lag only
Typical real-world delay 20-60 seconds sub-second to a few seconds
Requests for a 90-min match (15s interval) ~360 1 connection
Wasted requests (no change) most of them zero
For event-triggered logic (scratches, red cards, status changes): webhooks

Sometimes you don't want a constant stream — you want to be told only when a specific thing happens, and have your own server act on it without a client connection open at all:

javascript
// Express.js webhook receiver
app.post("/webhooks/orbistats", (req, res) => {
const { event, fixture_id, old_odds, new_odds } = req.body;

if (event === "odds.changed") {
// trigger downstream logic immediately — no polling anywhere
notifySubscribedUsers(fixture_id, new_odds);
}

res.sendStatus(200);
});

This is the right fit for anything discrete and infrequent — a scratch announcement, a fixture postponement, a final result — where you don't need a constant stream, just a guaranteed instant nudge when it matters.

Python side, for completeness

If your backend is Python rather than Node:

python
import asyncio
import websockets
import json

async def stream_live_scores(fixture_id):
uri = "wss://stream.orbistats.com/v1/live"
async with websockets.connect(uri) as ws:
await ws.send(json.dumps({
"action": "subscribe",
"channel": "live_scores",
"fixture_id": fixture_id
}))
async for message in ws:
update = json.loads(message)
print(f"Score update: {update['score']}")
# push to your own downstream consumers here

asyncio.run(stream_live_scores("fixture_50231"))
What actually determines your remaining delay

Once you've switched to WebSocket/webhooks, the delay you have left is almost entirely the provider's collection-to-publish speed — which is exactly the metric worth interrogating before picking a provider. This is the same distinction a firm evaluating market-making feeds cares about: the important number is event-to-feed latency (venue to the feed), not your API response time, since your API response time is now near-zero with a push architecture.

When REST polling is still fine

To be clear — polling isn't always wrong. It's the right choice when:

You need standings, fixtures, or historical data that doesn't change every few seconds.
You're fine with data that's a few minutes stale (pre-match research, dashboards refreshed on page load).
You want simplicity over real-time precision for a low-traffic internal tool.

The mistake isn't using REST — it's using REST for something that's inherently a stream.

Where this fits with Orbistats

Our live scores API is available over REST for on-demand pulls, but the actual fix for the "30 seconds late" problem is our WebSocket API and webhooks, built specifically so you're not reconstructing the polling pattern above on your own infrastructure. You can test both directly in our public sandbox with no signup, check code samples across eight languages in the documentation, and see the API reference for exact message schemas. If you're evaluating this for trading or market-making specifically, our odds API and research on what sub-50ms actually requires end to end go into the latency question in more depth. Check our status page for live uptime, our changelog for what's shipped recently, and pricing for which tier includes full WebSocket access versus delayed REST-only data.

Top comments (0)