OpenAI · Anthropic · Gemini — one line for Python LLM workers
Your LLM provider rate-limits you, then goes down.
What happens to your jobs?
Every worker retries into the same 429, hung calls tie up the pool, and the jobs that failed in the outage are gone. Wrap your client once: the workers share one wait, and every job the outage stopped is kept — and comes back.
- 12
- jobs stopped by an outage
- 12
- came back
- 0
- lost
- 3 / 3
- runs, same result
Measured against the Gemini API free tier, outage injected by a local proxy: every job that came back got a real model answer. Without it, a job out of retries is simply gone.
import baldur
from openai import OpenAI
llm = baldur.llm.wrap(OpenAI(), timeout=60.0)
@baldur.protected("summarize", replay=True)
def summarize(doc_id: str) -> str:
reply = llm.chat.completions.create(
model="gpt-4o-mini", messages=prompt(doc_id))
return reply.choices[0].message.content
-
1 · provider down
- 50312 jobs failed in 1.3 s — 8 after every endpoint was tried, 4 rejected by the open breaker without calling the provider
- 50312 parked with their arguments
-
2 · back
- 200the provider is back — the breaker waits, probes, closes
- 200breaker closed → 12 parked jobs re-run automatically (dlq pending 0)
- 200parked 12 · replayed 12 · lost 0
The outage half of the demo's own output: the real openai SDK against a local fake provider, eight threads as the workers. Against the real Gemini API the same scene gave the same counts.
Run it yourself — one process, no Redis, no broker, no API key:
pip install "baldur-framework[celery]" openai
python -m baldur.scripts.demo_llm_outage
One line around your client, not a gateway to run.
-
01 — wrap
Wrap the client you already have
The OpenAI SDK, Anthropic, google-genai. Each provider error is sorted: wait, move to your next endpoint, or fail now.
-
02 — share
One wait for every worker
A 429 or an overload installs one wait the workers share — across processes through Redis — and the breaker keeps a dead provider from holding the pool.
-
03 — replay
Stopped jobs come back
A job no endpoint could answer is parked with its arguments and re-runs once the provider answers — on a Celery worker, or from the console with a click. A replay runs the whole job again: make it safe to run twice.
Your stack already fights failures —
in five places that don't talk.
Each layer solves its slice and drops the rest.
| Layer | Retry | Circuit breaker | Keeps the failed call | Replays it on recovery | Where it stops |
|---|---|---|---|---|---|
| openai / anthropic SDK retries | yes | no | no | no | Each worker waits alone; a job out of retries is gone. |
| tenacity + pybreaker | yes | yes | no | no | Wiring the two together is on you. |
| Celery / SQS dead letters | yes | no | yes | no | Parks the job, then waits for a human. |
| Envoy / Istio | yes | yes | no | no | Sees connections, not your call’s arguments. |
| Temporal | yes | no | yes | yes | Once you move the code into its workflow model. |
| baldur.llm.wrap + replay=True | yes | yes | yes | yes, with the prerequisites in the note below the table | One line, in your process. |
* Automatically on a Celery worker that imports the job; from the console otherwise. What it needs →
The overhead is measured —
not promised.
Every figure is a dated measurement on a stated topology.
- Per protected call, in-memory ~39 µs decorator alone, no network
- Per protected call, over Redis +1.4–1.8 ms server CPU, idempotency on, below saturation
- Per-request overhead +1.1% throughput, below saturation
- Dead-letter entry, stored ~953 B of Redis per parked call
Topology moves all of these. How each was measured, and the one we withdrew →