If you are polling ESPN every 5 seconds for live scores, you are doing it the hard way. WebSockets give you push-based updates the instant something changes — no wasted requests, no missed events, lower latency. We'll use the websockets Python library.
HTTP Polling: The Baseline
Most people start here. Hit the ESPN scoreboard API on a loop:
import requests
import time
def poll_scores(sport="basketball", league="nba", interval=5):
url = f"https://site.api.espn.com/apis/site/v2/sports/{sport}/{league}/scoreboard"
while True:
data = requests.get(url).json()
for event in data["events"]:
if event["status"]["type"]["state"] == "in":
home = event["competitions"][0]["competitors"][0]
away = event["competitions"][0]["competitors"][1]
print(f"{away['team']['abbreviation']} {away['score']} @ "
f"{home['team']['abbreviation']} {home['score']}")
time.sleep(interval)
Three problems: 5-second latency ceiling, wasted requests when nothing changes, and rate limits across multiple sports. For trading bots, that latency window is where money is made and lost.
WebSocket Approach: Push-Based Updates
A WebSocket connection stays open. The server pushes data when something changes:
import asyncio
import websockets
import json
async def stream_scores(uri, callback):
async for ws in websockets.connect(uri):
try:
async for message in ws:
await callback(json.loads(message))
except websockets.ConnectionClosed:
print("Reconnecting...")
continue
The async for ws in websockets.connect(uri) pattern handles reconnection automatically — critical for multi-hour game sessions.
Building a WebSocket Bridge
If your data source only offers HTTP (like ESPN), build a bridge: one process polls, another broadcasts changes over a local WebSocket:
import asyncio
import websockets
import json
import aiohttp
CLIENTS = set()
async def espn_poller():
prev_states = {}
url = "https://site.api.espn.com/apis/site/v2/sports/basketball/nba/scoreboard"
async with aiohttp.ClientSession() as session:
while True:
async with session.get(url) as resp:
data = await resp.json()
for event in data.get("events", []):
game_id = event["id"]
state = {
"game_id": game_id,
"home": event["competitions"][0]["competitors"][0]["team"]["abbreviation"],
"away": event["competitions"][0]["competitors"][1]["team"]["abbreviation"],
"home_score": int(event["competitions"][0]["competitors"][0]["score"]),
"away_score": int(event["competitions"][0]["competitors"][1]["score"]),
"period": event["status"]["period"],
"clock": event["status"]["displayClock"],
}
if state != prev_states.get(game_id):
prev_states[game_id] = state
msg = json.dumps(state)
await asyncio.gather(*[c.send(msg) for c in CLIENTS])
await asyncio.sleep(3)
async def handler(ws):
CLIENTS.add(ws)
try:
await ws.wait_closed()
finally:
CLIENTS.discard(ws)
async def main():
async with websockets.serve(handler, "localhost", 8765):
await espn_poller()
asyncio.run(main())
Your trading bot connects to ws://localhost:8765 and receives only changed game states — no parsing, no polling logic, no rate limit management.
The Trading Bot Client
async def trading_bot():
async for ws in websockets.connect("ws://localhost:8765"):
try:
async for message in ws:
game = json.loads(message)
fair_prob = model.predict(extract_features(game))
market_price = get_market_price(game["game_id"])
edge = fair_prob - market_price
if edge > 0.08:
print(f"SIGNAL: {game['away']}@{game['home']} "
f"fair={fair_prob:.2f} market={market_price:.2f}")
except websockets.ConnectionClosed:
continue
Every score change triggers immediate model evaluation. No waiting for the next poll cycle.
Latency Comparison
| Approach | Median Latency | P95 Latency |
|---|---|---|
| HTTP polling (5s) | 2.8s | 5.0s |
| HTTP polling (2s) | 1.2s | 2.0s |
| WebSocket bridge (3s poll) | 1.6s | 3.1s |
The WebSocket bridge sits in the middle — better than 5-second polling, not as fast as a native feed. For most strategies, sub-2-second latency is sufficient because prediction market orderbooks take 5-30 seconds to fully reprice after a scoring event.
Handling Connection Failures
from datetime import datetime, timedelta
async def resilient_stream(uri, callback, max_gap=timedelta(seconds=30)):
last_msg = datetime.now()
async for ws in websockets.connect(uri, ping_interval=20, ping_timeout=10):
try:
async for message in ws:
last_msg = datetime.now()
await callback(json.loads(message))
except websockets.ConnectionClosed:
if datetime.now() - last_msg > max_gap:
print("Data gap detected — backfilling via HTTP")
await backfill_current_state(callback)
continue
If your WebSocket was down for 30+ seconds during a live game, you may have missed scoring events. The HTTP backfill catches your model up so it does not trade on stale data.
When to Use Which
HTTP polling works for backtesting, historical scraping, and low-frequency monitoring. WebSocket streaming is better when you trade on live prediction markets, track many concurrent games, or need sub-second reaction times.
For production trading bots, the bridge architecture gives you the best balance: simple ESPN polling on the backend, instant delivery to your bot on the frontend.
Part of the ZenHodl blog. We write about sports analytics, prediction markets, and building trading bots with Python.