Domain-neutral multi-API failover engine: try providers in order, fall back per-call or per-item, with provenance tagging and a circuit breaker. Zero dependencies.
Project description
apifailover
Your integration stops dying when one API has a bad day.
When the API you depend on rate-limits, times out, or returns garbage, your code
shouldn't go down with it. apifailover takes an ordered list of providers and
tries them in order — and when a provider answers only some of what you asked
for, it fills the rest from the next one. Every value comes back tagged with
which provider served it.
Pure Python. Zero dependencies. Fully type-hinted. Works with any callable — prices, weather, geocoding, sports data, LLM endpoints — it doesn't know or care about your domain.
Package name: published as
apifailover(verified available on PyPI at time of writing). If it's been claimed by the time you publish, good alternatives — also available — arefailover-engineandmultifailover. (apiguardis taken.) Pick one and updatenameinpyproject.toml+ the install line below.
Why this exists
Everyone reinvents this badly: a bare try/except that swaps to a backup, no
provenance, naive infinite retry that hammers a dead endpoint. This is the
reusable version, with the two things people skip:
- Per-item failover — provider A returns 8 of 10 items, provider B fills the 2 missing ones. You get all 10, each tagged with its source.
- A circuit breaker — a provider that keeps failing is skipped for a cooldown instead of being retried forever, then probed once to see if it's back.
Install
pip install apifailover
Quick start
from apifailover import FailoverClient, Provider
client = FailoverClient(retries=1) # one retry per provider before failover
result = client.fetch([
Provider("primary", lambda: call_primary_api()),
Provider("backup", lambda: call_backup_api()),
])
print(result.value) # whatever the winning provider returned
print(result.provider) # "primary" or "backup" — provenance, for free
print(result.attempts) # ["primary"] or ["primary", "backup"]
If every provider fails you get one AllProvidersFailed that preserves each
provider's root cause (.errors is a {name: exception} mapping) — never a
single opaque error.
The differentiator: per-item failover
This is the feature most failover snippets don't have. Ask for a set of keys; each provider serves what it can; the next provider is asked only for what's still missing.
from apifailover import FailoverClient, ItemProvider
# Two fake price feeds. Primary knows the majors; backup has the long tail.
def primary(symbols):
book = {"BTC": 64000.0, "ETH": 3100.0}
return {s: book[s] for s in symbols if s in book}
def backup(symbols):
book = {"BTC": 63950.0, "AVAX": 38.2, "MATIC": 0.72}
return {s: book[s] for s in symbols if s in book}
client = FailoverClient()
res = client.fetch_items(
["BTC", "ETH", "AVAX"],
[ItemProvider("primary", primary), ItemProvider("backup", backup)],
)
res.values # {"BTC": 64000.0, "ETH": 3100.0, "AVAX": 38.2}
res.provenance # {"BTC": "primary", "ETH": "primary", "AVAX": "backup"}
res.missing # [] (use require_all=True to raise instead)
backup was only ever asked for AVAX — the key primary couldn't serve.
The circuit breaker
No naive infinite retry. After N consecutive failures a provider's circuit opens and it's skipped for a cooldown window; the first call after the window is a half-open probe — success closes it, failure re-opens it.
from apifailover import FailoverClient, Provider, CircuitBreaker
breaker = CircuitBreaker(failure_threshold=3, reset_after=60.0)
client = FailoverClient(circuit_breaker=breaker)
providers = [Provider("flaky", call_flaky), Provider("stable", call_stable)]
for _ in range(100):
result = client.fetch(providers)
# Once "flaky" has failed 3× in a row it's skipped for 60s — the request
# goes straight to "stable" instead of paying the timeout every time.
breaker.state("flaky") # "closed" | "open" | "half-open"
Reject bad-but-non-throwing results
Some APIs return 200 OK with junk (empty list, null, a stale sentinel).
Give is_valid a predicate; a result that fails it is treated as a failure and
triggers failover:
client = FailoverClient(is_valid=lambda r: bool(r)) # reject empty/None
client = FailoverClient(is_valid=lambda price: price > 0) # reject non-positive
For per-item calls, is_valid is applied per key — a provider's bad value for
one key just leaves that key open for the next provider.
Knobs
FailoverClient(...) |
Default | What it does |
|---|---|---|
retries |
0 |
Extra attempts on the same provider before failing over. |
retry_backoff |
0.0 |
Seconds slept between same-provider retries (scales linearly). |
timeout |
None |
Wall-clock soft deadline per provider; a slower return counts as a failure. |
is_valid |
not-None |
Predicate to reject bad-but-non-throwing results. |
circuit_breaker |
None |
A CircuitBreaker to skip chronically-failing providers. |
Scope & honesty
What this is: a small, dependency-free, synchronous failover/fallback layer you wrap around your own HTTP calls (or any callable).
What it is not (by design, to stay small and honest):
- Not async. Providers are called synchronously. Wrap blocking calls in your own executor if you need concurrency.
- Not a hard timeout.
timeoutis a wall-clock check after the call returns — it can't interrupt a call mid-flight (that needs threads/signals and is intentionally out of scope). It still protects you from a slow provider winning when a faster fallback exists. - Not an HTTP client. Bring your own
requests/httpx; this just decides which provider's result to use and falls back when one misbehaves.
Run the tests
pip install pytest
python3 -m pytest tests/ -q # 18 tests, fully offline
License
MIT © 2026 Ivan Fodjo
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file apifailover-0.1.0.tar.gz.
File metadata
- Download URL: apifailover-0.1.0.tar.gz
- Upload date:
- Size: 12.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f32ba2e68ff68fbce6be635bbcbb2f591904d10709e961a433bb948613b062a
|
|
| MD5 |
4507f1500122fee72f77ba6a303463af
|
|
| BLAKE2b-256 |
70e90c5ead2d7b5937dcb751339bad68ab4ebfbe8fee29cfbc9012cf379816c6
|
File details
Details for the file apifailover-0.1.0-py3-none-any.whl.
File metadata
- Download URL: apifailover-0.1.0-py3-none-any.whl
- Upload date:
- Size: 10.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e5b9f070c0a0951be2ace5038bc319a97b8347bc2c6b6fea45bb2245a2f3f402
|
|
| MD5 |
5b874209c0fe012dba761f2613edcf1d
|
|
| BLAKE2b-256 |
0470555a65b6e91c833291b4184eb163c9d5d752dbff803200aae6eeaee44756
|