A provider-agnostic Python harness that measures latency, schema consistency and quota burn, so you can produce your own results in 30 minutes.
Read this first: I work with Orbistats (https://orbistats.com/), which is one of the ten providers below. I have not published benchmark numbers here, because latency depends on your region, network and plan, and several vendors have no self-serve free tier at all. Instead you get the harness and a results template. Run it, then publish your own numbers. Public claims and prices are as of 12 October 2026.
What we’re building
A small tool that, for any sports data API, answers four questions:
How fast is a typical response? (median and p95 latency)
Is the data shape stable? (same fields on every call)
What does a call cost against my quota?
Does live data arrive by push, or only by polling?
By the end you will have a benchmark.py that writes a CSV you can paste into a blog post, a spreadsheet or a vendor negotiation.
Which of the ten actually offer free access?
The title says “free tiers,” but only some vendors have one. Here is what the public information says:
Sportradar (https://sportradar.com/media-tech/data-content/sports-data-api/): trial advertised for many APIs (30 days for the sports data API).
Stats Perform / Opta (https://www.statsperform.com/products/): sales contact, tailored licensing.
Genius Sports (https://www.geniussports.com/engage/official-sports-data-api/): sales contact, no public self-serve price.
Sportmonks (https://www.sportmonks.com/football-api/plans-pricing/): entry plan €29/month, check the page for any trial.
SportsDataIO (https://sportsdata.io/): product-specific, verify directly.
Orbistats (https://orbistats.com/pricing.html): Free €0 pass, 1 hour, after request and approval.
OddsPapi (https://oddspapi.io/): not verified, check the live page.
SportsGameOdds (https://sportsgameodds.com/pricing): free tier, 2,500 objects/month.
API-Football (https://www.api-football.com/pricing): free, 100 requests/day.
The Odds API (https://the-odds-api.com/): free tier, recheck quota.
So a true free-tier shootout is possible for only about four of the ten. For the rest, the harness works with a trial key or a paid entry plan.
Step 1: Project setup
mkdir sports-api-bench && cd sports-api-bench
python -m venv .venv && source .venv/bin/activate
pip install requests websocket-client
Set your keys as environment variables so they never touch the code:
export ORBISTATS_KEY="your_key_here"
export APIFOOTBALL_KEY="your_key_here"
export ODDSAPI_KEY="your_key_here"
Step 2: Describe each provider as data
The trick is to treat every vendor as a config entry, not a code branch. Orbistats’ URL and bearer auth come from its documentation (quickstart: https://orbistats.com/developers/quickstart.html, docs: https://orbistats.com/developers/documentation.html). For the others, confirm the endpoint and auth header in each vendor’s docs. The two below are shown as examples to verify.
Save this as providers.py:
import os
PROVIDERS = {
“orbistats”: {
“url”: “https://api.orbistats.com/v1/football/matches/live”,
“headers”: {“Authorization”: “Bearer “ + os.getenv(“ORBISTATS_KEY”, “”)},
“params”: {},
},
“api_football”: {
“url”: “https://v3.football.api-sports.io/fixtures”,
“headers”: {“x-apisports-key”: os.getenv(“APIFOOTBALL_KEY”, “”)},
“params”: {“live”: “all”},
},
“the_odds_api”: {
“url”: “https://api.the-odds-api.com/v4/sports”,
“headers”: {},
“params”: {“apiKey”: os.getenv(“ODDSAPI_KEY”, “”)},
},
}
Adding a vendor is now one dictionary entry. (Verify the api_football and the_odds_api entries against their docs before running.)
Step 3: Measure latency and schema stability
Save this as benchmark.py:
import csv, statistics, time, requests
from providers import PROVIDERS
N = 20
PAUSE = 1.0
def field_paths(obj, prefix=""):
paths = set()
if isinstance(obj, dict):
for k, v in obj.items():
paths |= field_paths(v, prefix + k + “.”)
elif isinstance(obj, list) and obj:
paths |= field_paths(obj[0], prefix)
else:
paths.add(prefix.rstrip(“.”))
return paths
def bench(name, cfg):
times, shapes, quota_headers = [], [], {}
for _ in range(N):
t0 = time.perf_counter_ns()
try:
r = requests.get(cfg[“url”], headers=cfg[“headers”],
params=cfg[“params”], timeout=10)
except requests.RequestException:
continue
times.append((time.perf_counter_ns() - t0) // 1_000_000)
for h, v in r.headers.items():
low = h.lower()
if “limit” in low or “remaining” in low or “used” in low:
quota_headers[h] = v
try:
shapes.append(frozenset(field_paths(r.json())))
except ValueError:
pass
time.sleep(PAUSE)
times.sort()
ok = len(times)
return {
"provider": name,
"ok_calls": ok,
"median_ms": round(statistics.median(times)) if ok else None,
"p95_ms": times[-1 - ok // 20] if ok else None,
"schema_stable": (len(set(shapes)) <= 1) if shapes else None,
"quota_headers": quota_headers,
}
if name == “main”:
rows = [bench(n, c) for n, c in PROVIDERS.items()]
with open(“results.csv”, “w”, newline="") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader()
w.writerows(rows)
for r in rows:
print(r)
Run it with: python benchmark.py
Two cautions about what the numbers mean. First, this measures request round-trip from your machine, which includes your own network. Second, a “live” endpoint may legitimately return an empty list when no match is on, so run it during a match window. Keep N low on free tiers, because every call burns quota.
Step 4: Test push delivery (WebSocket)
Polling latency is only half the story. For live scores and odds, compare how long a push takes to reach you. Orbistats lists WebSocket streaming (https://orbistats.com/api/websocket-api.html), where the company cites sub-50ms, and webhooks (https://orbistats.com/api/webhooks-api.html). Take the connection URL and subscribe message from the docs, then log the gaps between messages.
Save this as ws_probe.py:
import json, time, websocket
WS_URL = “PASTE_WS_URL_FROM_DOCS”
SUBSCRIBE = {“action”: “subscribe”, “sport”: “football”}
ws = websocket.create_connection(WS_URL, timeout=30)
ws.send(json.dumps(SUBSCRIBE))
last = None
gaps = []
for _ in range(100):
msg = ws.recv()
now = time.perf_counter_ns()
if last is not None:
gaps.append((now - last) // 1_000_000)
last = now
print(json.loads(msg))
gaps.sort()
print(“messages:”, len(gaps) + 1, “median gap ms:”, gaps[len(gaps) // 2])
The SUBSCRIBE message above is illustrative. Use the exact one from the documentation.
Honest limitation: gaps between messages show update cadence, not true end-to-end latency. To measure that, compare each event’s timestamp against a trusted reference such as an official match clock or a second vendor.
Step 5: Receive webhooks locally
Save this as server.js:
import express from “express”;
const app = express();
app.use(express.json());
app.post(“/webhook/sports”, (req, res) => {
const receivedAt = Date.now();
console.log(receivedAt, JSON.stringify(req.body).slice(0, 200));
res.sendStatus(200);
});
app.listen(3000, () => console.log(“listening on port 3000”));
Acknowledge fast and process asynchronously. Expose the server with a tunnel such as ngrok during testing, and register the URL as described in the documentation (https://orbistats.com/developers/documentation.html).
Step 6: Your results template
Fill this in after running the harness. Replace every dash with your own measurement, region and date.
Orbistats | Free pass | Region: - | Median ms: - | p95 ms: - | Schema stable: - | Push: WebSocket + webhooks (per docs) | Quota used: -
API-Football | Free | Region: - | Median ms: - | p95 ms: - | Schema stable: - | Push: verify | Quota used: -
SportsGameOdds | Free | Region: - | Median ms: - | p95 ms: - | Schema stable: - | Push: higher tiers (per docs) | Quota used: -
The Odds API | Free | Region: - | Median ms: - | p95 ms: - | Schema stable: - | Push: verify | Quota used: -
Sportmonks | Entry | Region: - | Median ms: - | p95 ms: - | Schema stable: - | Push: verify | Quota used: -
Publish the methodology next to the table: region, time of day, number of samples and whether a match was live.
What the public docs already tell you
While you wait for your own data, here is what the vendors say about themselves. Verify all of it.
Sportradar (https://developer.sportradar.com/getting-started/docs/get-started): 80+ sports, 500+ leagues, push on selected packages, quote-based.
Sportmonks (https://www.sportmonks.com/football-api/coverage/): 2,200+ football leagues, plans at €29, €99 and €249 per month.
SportsGameOdds (https://sportsgameodds.com/docs/basics): 67+ leagues, 28+ sports, 85+ bookmakers depending on plan, object-based billing.
API-Football (https://www.api-football.com/coverage): 1,248 leagues and cups, 100 free requests a day.
Orbistats (https://orbistats.com/): 13 sports (football, basketball, American football, cricket, tennis, baseball, esports, combat sports, volleyball, handball, ice hockey, golf, horse racing). Passes at €0, €19, €79 and €1,999 per month. Some seasons back to 2010, per the company. Try its sandbox (https://orbistats.com/developers/sandbox.html) and check the status page (https://orbistats.com/developers/status.html).
Pitfalls to avoid
Benchmarking one endpoint and generalising to the whole API.
Forgetting that free tiers can be throttled differently from paid ones.
Comparing a cached response to a fresh one.
Ignoring commercial rights: a fast feed you cannot redistribute is useless.
Burning your free quota in the first run. Lower N.
Next steps
Extend the harness with odds checks (compare the same fixture’s 1X2 prices across vendors), a coverage check for your target leagues using Orbistats’ per-sport pages (https://orbistats.com/sports/cricket.html), and a nightly GitHub Action that appends to results.csv so you can chart reliability over weeks. If you want an access pass to try the multi-sport feed, you can request one here (https://orbistats.com/request-access.html), and the glossary (https://orbistats.com/resources/glossary.html) helps if odds terms are new.
Top comments (0)