Recovering from failures¶
An agent task can fail partway through a run: the model provider goes down or rate
limits the task, or the worker running it dies. Two settings keep the work the agent
already did: fallback_conn_ids moves a failed model request to another connection,
and durable=True lets an Airflow retry replay the steps an earlier attempt
completed. To decide whether a failure is worth retrying at all, see
Retry policies.
What each failure costs¶
Failure |
Handled by |
What happens to the completed steps |
|---|---|---|
The provider returns any HTTP 4xx or 5xx error, times out, or cannot be reached |
|
Kept. The next connection receives the run’s message history, including every tool result, and continues the run. |
Every connection in the chain fails, and the task raises
|
Airflow |
Replayed. The retry replays the steps an earlier attempt recorded, whichever connection answered them, and continues on the first connection that answers now. |
The worker dies, or the task hits its |
Airflow |
Replayed, as above. A step that was still running runs again. |
Either of the last two, without |
Airflow |
Run again from the start: every model call is paid for again and every tool runs again. |
A response the provider does return but pydantic-ai rejects, such as a content-filter
refusal (ContentFilterError), does not fail over: the chain moves on only for
ModelAPIError.
The OpenAI and Anthropic SDKs retry a connection error, a timeout, and a 408, 409, 429 or
5xx response twice by default before the chain sees the failure. A refused connection
therefore costs a few seconds before failover, and a request that hangs costs up to three
times the timeout in model_settings, plus backoff. Without a timeout, each
attempt waits for the SDK’s default of 600 seconds. Both SDKs also wait out a 429’s
Retry-After header before retrying: the OpenAI SDK for up to two minutes, the
Anthropic SDK for as long as the header asks.
On Airflow before 3.3 the journal is a file under [common.ai] durable_cache_path; a
retry that runs on another worker replays only if that path is shared storage. See
Durable execution.
Keep a run going through an outage¶
This task declares both settings: a standby connection for a failed request, and a journal for a failed attempt:
if SQLToolset is not None:
@dag(
default_args={"retries": 2, "retry_delay": timedelta(minutes=1)},
catchup=False,
tags=["example", "fallback", "durable"],
)
def example_agent_recovery():
AgentOperator(
task_id="investigate_orders",
prompt="Which region placed the most orders, and what share of all orders is that?",
llm_conn_id="llm_primary",
fallback_conn_ids=["llm_standby"],
durable=True,
system_prompt="You are a SQL analyst. Use the tools to answer with data.",
toolsets=[SQLToolset(db_conn_id="warehouse", allowed_tables=["orders"], max_rows=20)],
)
See Provider fallback for the chain itself, including how a bare model name is
forwarded to connections that set no model of their own. See
Durable execution for the journal, including the id it needs on every
FunctionToolset, MCPToolset and DynamicToolset.
What the next connection receives¶
Failover happens per model request. Each request carries the run’s whole message history: the prompt, every model response so far and every tool result. The next connection in the chain gets that same history and continues the run; a tool that already ran is not called again. The next connection can be another vendor’s model, for example an Anthropic model taking over a run an OpenAI model started.
A streamed request is the exception. When the run streams, for example with an
event_stream_handler in agent_params, the chain moves on only if the stream fails
to open; an error after the stream started propagates and fails the attempt.
To drill the example, point llm_primary at a port nothing listens on. The task
succeeds, and its log shows the chain, the OpenAI SDK’s two retries against the primary,
and the model that finished the run:
Resolving LLM fallback chain: llm_primary -> llm_standby
Retrying request to /responses in 0.459459 seconds
Retrying request to /responses in 0.913650 seconds
::group::Tool call: list_tables
Retrying request to /responses in 0.485229 seconds
Retrying request to /responses in 0.849597 seconds
::group::Tool call: get_schema
...
Durable: replayed 0 steps (0 model, 0 tool, 0 other), recorded 7 new steps (4 model, 3 tool, 0 other)
::group::LLM run complete: model=claude-sonnet-5, requests=4, tool_calls=3, input_tokens=5135, output_tokens=334, total_tokens=5469
Tool call sequence: list_tables -> get_schema -> query
Each of the run’s four model requests tried the primary first and paid its retries
before moving on. The chain keeps no memory of an outage between requests, so keep the
primary’s timeout short.
Retries after the whole chain fails¶
With durable=True, each step is recorded in the task instance’s journal whichever
connection answered it. When every connection fails, the task raises
FallbackExceptionGroup and Airflow retries it. The retry replays the recorded steps,
then sends its first live request through the chain from the top. The run continues on
the first connection that answers, whether or not that is the one that answered before.
A recorded model step replays only if the retry’s request has the same fingerprint, and
for a chain the model in the fingerprint is the whole chain, whichever connection
answered. Adding, removing or reordering a connection in fallback_conn_ids, or
changing the model of a connection in it, between attempts therefore makes the retry
run every step live, tools included. Changing a connection’s credentials or host does
not. “How a retry replays” in Durable execution gives the full matching rules.
What is not covered¶
A tool that sent an email and then raised sends it again on retry, and so does one whose worker died before its result was recorded. Make such tools idempotent; see “Side effects and idempotency” in Durable execution.
A request every provider rejects, such as a malformed one that each returns a 400 for, walks the whole chain on every attempt. A
RetryRulecannot stop those retries without stopping outage retries too: once a chain is configured, both raiseFallbackExceptionGroup, and a rule compares only that top-level type. Retry policies need Airflow 3.3 or later; see “When the connection also carries a fallback chain” in Retry policies.No connection type builds the
AnthropicVertexclient that Claude on Vertex AI needs, so a chain cannot include Claude on Vertex AI. A Vertex AI connection reaches Gemini.Failover and replay cover pydantic-ai agents only:
fallback_conn_idson the pydantic-ai hooks and the operators built on them, and replay throughdurable=Trueor theAirflowDurabilitycapability in a plain@task. A Strands Agents agent run throughinvoke_durablyresumes from its own checkpoint on Airflow 3.3 and later (Durable execution with Strands), and a retriedLLMBatchOperatorre-attaches to its running batch (Retries re-attach instead of re-submitting).A
ManagedAgentToolsetruns again on every attempt unless it is built withreplayable=True. Durable execution cannot be combined withenable_hitl_review, aSandboxToolsetor tool approval; see the limitations in Durable execution.