Overview
Sports APIs fail. Servers go down during peak traffic, rate limits get exhausted, and maintenance windows take endpoints offline. If your application depends on a single provider, a provider outage is your outage. A failover system automatically switches to a backup provider when the primary is unavailable, keeping your app running.
The challenge is that different providers return data in different formats. A failover system must not only detect failures and switch providers but also normalize the response so your application code does not need to know which provider served the request.
When you need failover: If you are running a production sports app with SLA requirements, betting platforms, or any service where downtime means lost revenue. Use the comparison tool to find providers with overlapping coverage.
Architecture
A failover system has four components:
Failover Architecture
Client Request
│
▼
┌─────────────┐
│ Failover │──→ Health Check ──→ Provider A (Primary)
│ Router │──→ Health Check ──→ Provider B (Backup)
│ │──→ Health Check ──→ Provider C (Fallback)
└─────┬───────┘
│
▼
┌─────────────┐
│ Normalizer │ Maps provider-specific response
│ │ to a common schema
└─────┬───────┘
│
▼
Unified Response → Client
Health checks run every 30s:
- Track success rate per provider
- Mark provider as "down" after 3 consecutive failures
- Re-enable after 2 consecutive successes
- Circuit breaker per providerImplementation
Failover Router (JavaScript)
Here is a failover router that tries providers in priority order, with per-provider health tracking and circuit breaker logic:
// failoverRouter.js
class ProviderHealth {
constructor(name) {
this.name = name;
this.failures = 0;
this.successes = 0;
this.isOpen = false; // Circuit breaker state
this.lastFailure = null;
this.openedAt = null;
}
recordSuccess() {
this.successes++;
if (this.isOpen && this.successes >= 2) {
this.isOpen = false;
this.failures = 0;
console.log(`[${this.name}] Circuit closed — recovered`);
}
}
recordFailure() {
this.failures++;
this.successes = 0;
this.lastFailure = Date.now();
if (this.failures >= 3 && !this.isOpen) {
this.isOpen = true;
this.openedAt = Date.now();
console.log(`[${this.name}] Circuit opened — failing`);
}
}
isAvailable() {
if (!this.isOpen) return true;
// Half-open: allow one test request after 30s cooldown
if (Date.now() - this.openedAt > 30_000) {
console.log(`[${this.name}] Half-open — testing...`);
return true;
}
return false;
}
}
class FailoverRouter {
constructor(providers) {
// providers: [{ name, priority, fetchFn, normalizeFn }]
this.providers = providers.map(
(p) => ({ ...p, health: new ProviderHealth(p.name) })
);
}
async fetch(...args) {
// Sort by priority (1 = highest)
const sorted = [...this.providers].sort(
(a, b) => a.priority - b.priority
);
for (const provider of sorted) {
if (!provider.health.isAvailable()) {
console.log(`[${provider.name}] Skipped — circuit open`);
continue;
}
try {
const raw = await provider.fetchFn(...args);
provider.health.recordSuccess();
// Normalize the response to a common format
const normalized = provider.normalizeFn(raw);
normalized._provider = provider.name;
return normalized;
} catch (err) {
provider.health.recordFailure();
console.error(
`[${provider.name}] Failed: ${err.message}`
);
// Try next provider
}
}
throw new Error("All providers failed — no data available");
}
}
// Define providers with their fetch and normalize functions
const router = new FailoverRouter([
{
name: "API-Sports",
priority: 1,
async fetchFn(endpoint, params) {
const url = new URL(
`https://v3.football.api-sports.io/${endpoint}`
);
Object.entries(params).forEach(([k, v]) =>
url.searchParams.set(k, v)
);
const res = await fetch(url, {
headers: { "x-apisports-key": process.env.API_SPORTS_KEY },
});
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
normalizeFn(data) {
return {
fixtures: data.response.map((f) => ({
id: f.fixture.id,
date: f.fixture.date,
status: f.fixture.status.short,
homeTeam: f.teams.home.name,
awayTeam: f.teams.away.name,
homeScore: f.goals.home,
awayScore: f.goals.away,
})),
};
},
},
{
name: "API-Football",
priority: 2,
async fetchFn(endpoint, params) {
const url = new URL(
`https://api.apifootball.com/?action=get_${endpoint}`
);
url.searchParams.set("APIkey", process.env.API_FOOTBALL_KEY);
Object.entries(params).forEach(([k, v]) =>
url.searchParams.set(k, v)
);
const res = await fetch(url);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return res.json();
},
normalizeFn(data) {
// API-Football returns a different structure
return {
fixtures: (Array.isArray(data) ? data : []).map((f) => ({
id: f.match_id,
date: f.match_start,
status: f.match_status,
homeTeam: f.match_hometeam_name,
awayTeam: f.match_awayteam_name,
homeScore: parseInt(f.match_hometeam_score) || 0,
awayScore: parseInt(f.match_awayteam_score) || 0,
})),
};
},
},
]);
// Usage — the router handles failover automatically
const result = await router.fetch("fixtures", {
league: 39,
season: 2025,
});
console.log(`Served by: ${result._provider}`);Background Health Checker (Python)
A background health checker proactively tests each provider every 30 seconds, so failover decisions are based on recent health rather than waiting for a user-facing request to fail:
# health_checker.py
import asyncio
import time
import httpx
class HealthChecker:
def __init__(self, providers):
self.providers = providers # [{name, url, api_key, headers}]
self.health = {
p["name"]: {
"available": True,
"failures": 0,
"last_check": 0,
"latency_ms": 0,
}
for p in providers
}
async def check_provider(self, provider):
try:
start = time.monotonic()
async with httpx.AsyncClient() as client:
resp = await client.get(
provider["health_url"],
headers=provider.get("headers", {}),
timeout=5,
)
latency = (time.monotonic() - start) * 1000
if resp.status_code == 200:
self.health[provider["name"]].update({
"available": True,
"failures": 0,
"last_check": time.time(),
"latency_ms": round(latency),
})
else:
self._record_failure(provider["name"])
except Exception:
self._record_failure(provider["name"])
def _record_failure(self, name):
h = self.health[name]
h["failures"] += 1
h["last_check"] = time.time()
if h["failures"] >= 3:
h["available"] = False
print(f"[{name}] Marked as unavailable")
async def run_forever(self, interval=30):
while True:
tasks = [self.check_provider(p) for p in self.providers]
await asyncio.gather(*tasks, return_exceptions=True)
await asyncio.sleep(interval)
def get_healthy_providers(self):
"""Return provider names sorted by health and latency."""
healthy = [
(name, info)
for name, info in self.health.items()
if info["available"]
]
return sorted(healthy, key=lambda x: x[1]["latency_ms"])
# Usage
providers = [
{
"name": "API-Sports",
"health_url": "https://v3.football.api-sports.io/status",
"headers": {"x-apisports-key": "your-key"},
},
{
"name": "Football-Data.org",
"health_url": "https://api.football-data.org/v4/competitions",
"headers": {"X-Auth-Token": "your-token"},
},
]
checker = HealthChecker(providers)
# Start health checker in background
asyncio.create_task(checker.run_forever(interval=30))Best Practices
Choose providers with overlapping coverage
Your backup provider must support the same sports, leagues, and data types as your primary. Use the comparison tool to find providers with matching coverage.
Normalize at the adapter layer
Each provider adapter should convert responses to a common schema. Your application code should never check which provider served the data. See our data normalization guide for patterns.
Use circuit breakers, not just retries
Retrying a failing provider wastes time and can make things worse. A circuit breaker stops sending requests to a failing provider after a threshold, giving it time to recover. Combine with our error handling patterns.
Log which provider served each request
Include a _provider field in responses for observability. This helps you understand how often failover is triggered and whether your primary provider is reliable enough.
Related Guides
Error Handling & Retry Strategies
14 min readHandling API Rate Limits
13 min readCaching Strategies for Sports API Data
12 min readFind backup providers for your stack
Compare providers with overlapping sports and league coverage.