Skip to main content

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 — are failover-engine and multifailover. (apiguard is taken.) Pick one and update name in pyproject.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. timeout is 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

apifailover-0.1.0.tar.gz (12.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

apifailover-0.1.0-py3-none-any.whl (10.4 kB view details)

Uploaded Python 3

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

Hashes for apifailover-0.1.0.tar.gz
Algorithm Hash digest
SHA256 3f32ba2e68ff68fbce6be635bbcbb2f591904d10709e961a433bb948613b062a
MD5 4507f1500122fee72f77ba6a303463af
BLAKE2b-256 70e90c5ead2d7b5937dcb751339bad68ab4ebfbe8fee29cfbc9012cf379816c6

See more details on using hashes here.

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

Hashes for apifailover-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 e5b9f070c0a0951be2ace5038bc319a97b8347bc2c6b6fea45bb2245a2f3f402
MD5 5b874209c0fe012dba761f2613edcf1d
BLAKE2b-256 0470555a65b6e91c833291b4184eb163c9d5d752dbff803200aae6eeaee44756

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page