Skip to main content

Pydantic AI Sticky Fallback Explained: Let the Backup Start the Next Turn

A backup model is less useful if every new message first waits for the same broken primary. I would use sticky fallback to remember a working route within a conversation, while keeping that routing choice separate from service health and permission to execute tools.

This guide explains Pydantic AI’s ModelFallback(..., sticky=True) through a small support-chat example. We will follow the history that carries the choice, test what happens when the backup fails, identify the streaming boundary, and budget nested retries without assuming a cache hit or an immediate failover.

Pydantic AI sticky fallback: primary A fails, backup B answers, saved history makes B the next default, and failure can return routing to A

Why an ordinary fallback can repeat the same delay

Pydantic AI is a Python framework for applications that ask models for answers and may let them request tools. A primary model is the preferred starting choice. A fallback is another candidate tried after an eligible failure. A retry instead repeats an attempt at the same layer, such as another request to the same provider.

A request step is one point in an agent run that asks a model for a response. A single user turn can contain several request steps, for example when a model requests a tool and then reads its result.

Imagine a support assistant that drafts a reply, shortens it on request and then translates it. Model A is unavailable and model B can answer. Without a remembered route, each new request step can start on A again, wait for failure and then reach B. The fallback works, but the conversation repeatedly pays for the failed start.

Read Figure 1: Compare the second turn, not just the first successful answer. Sticky behavior matters when another request uses the preserved conversation history.

A fallback can change where the next turn startsA fails and B answers in both modes. On the next turn with the same history, sticky off tries A first again, while sticky on starts on B.A fallback can change where the next turn startsConceptual two-turn comparison. Arrows are request order, not elapsed time.SAME CONVERSATION · A IS STILL UNAVAILABLETURN 1 · BOTH MODESA fails → B answersPreserve the responseand failed-attempt recordTURN 2 · STICKY OFFTry A again → failThen ask BTURN 2 · STICKY ONStart on BUse restored historyThe saved response changes the next default starting point.The user’s next message still belongs to the same conversation.
Remember the working backupA fails and B answers in both modes. On the next turn with the same history, sticky off tries A first again, while sticky on starts on B.Remember the working backupConceptual comparison of two settings.TURN 1 · BOTH MODESA fails → B answersKeep model identity and failuresinside the message historyALTERNATIVE NEXT-TURN ROUTESTURN 2 · STICKY OFFA fails again → B answersRepeat the failed startTURN 2 · STICKY ONB starts directlyHistory carries the evidenceSame conversation.Different next default route.No latency measurements shown.
Figure 1. Conceptual conversation example with A unavailable and B working. Sticky routing avoids the repeated default start only when the relevant history is carried forward. Original diagram: Tech Trend Insight. Source context: ModelFallback documentation.

Here sticky means that an eligible previous fallback success influences the next default starting model. It does not mean that every user shares the same remembered outage or that B has become the permanent primary. The unit to reason about is the conversation and its recorded responses.

What changed, and which release date to record

The project’s merged sticky-fallback proposal was merged October 30, 2026. The PyPI 2.64.0 release record lists the package’s upload and release date as October 31. These are different milestones. Pin the installed version when reproducing behavior; the implementation checks for this article used pydantic-ai-slim==2.64.0, the core distribution without the full set of optional provider packages.

The feature is off by default. The ModelFallback documentation explains that sticky=True uses previous model responses to choose a working candidate for later default requests. That can avoid repeatedly starting with a failing model. It changes where the chain begins; it does not make B’s responses equivalent in quality, tools or price to A’s.

The history is evidence, not just a transcript

A model response includes more than visible text. The response-field reference describes model_id, which records the model requested for that response, and failed_attempts, which records abandoned attempts before the answer. The identifier is routing evidence, not proof of an immutable provider checkpoint or a correct answer.

The sticky feature reads the relevant response history; it does not need a global “last working model” variable and does not stamp a separate sticky flag into arbitrary message metadata. Keep the framework’s typed messages when storing history. A typed message has defined fields that can be validated and restored, rather than only a role and a text string.

Read Figure 2: Follow the response fields through storage into a new worker. Recreating an agent is compatible with this design; losing the relevant message fields is the dangerous boundary.

The memory travels with the conversationWorker one stores typed message history containing the serving model and prior failed attempts. Worker two restores that history and uses it with a recreated agent. A store version prevents accidental concurrent overwrites.The memory travels with the conversationProposed persistence boundary. The capability itself has no cross-run state.WORKER 1A fails; B answersResponse: model_id = Bfailed_attempts contains ASave complete messagesHISTORY STORETyped messages → JSONConversation ID + versionPreserve response fieldsProtect and isolate recordsWORKER 2Load typed messagesRecreate the agentPass message_historyDefault starts on BA text-only transcript can lose routing evidence.Do not depend on a process-wide “last good model” variable.
Carry the routing evidenceWorker one stores typed message history containing the serving model and prior failed attempts. Worker two restores that history and uses it with a recreated agent. A store version prevents accidental concurrent overwrites.Carry the routing evidenceProposed persistence boundary.WORKER 1A fails; B answersKeep model_id + failed_attemptsSerialize the full typed historyHISTORY STOREConversation ID + versionJSON with response fieldsNo cross-user mixingWORKER 2Restore typed messagesPass message_historyStart the default on BPlain text can lose the evidence.Keep concurrent turns ordered.
Figure 2. Conceptual persistence design. Conversation ID, storage protection and version checks are application responsibilities; the framework supplies typed history serialization. Original diagram: Tech Trend Insight. Source context: message-history guide; merged sticky-fallback proposal.

The message-history guide provides ModelMessagesTypeAdapter for serialization and validation. Serialization converts messages into a storable form such as JSON, a structured text format; restoration reconstructs the typed records. Save the full history needed by the documented routing logic and pass it as message_history on the next run.

This creates an application responsibility. Isolate histories by conversation and user. If two turns read the same saved version and both overwrite it, one response can disappear along with its routing evidence. Serialize turns within a conversation, or use a version check before committing a new history. Test any summary, history compactor or migration that changes these records; a plain-text summary cannot be assumed to preserve routing state.

Stay on B, then wrap around when B fails

In an ordinary A → B → C configuration, suppose A fails and B answers. Later defaults can start on B. If B later fails, the chain tries C, then wraps to A and any remaining earlier candidates. Candidates already attempted in that request step are skipped. If A answers again, subsequent defaults can start on A.

Read Figure 3: A successful B response keeps the route on B. The route back to A is triggered by the chain’s results, not by an elapsed timer.

Sticky is a routing state, not a permanent lockA fallback success on B makes B the next default. B successes preserve the streak. If B fails, try C then A. When A answers, A becomes the next default. There is no timer-based expiry.Sticky is a routing state, not a permanent lockConceptual chain A → B → C. Example assumes ordinary unsuspended requests.PREFERRED AA fails → B succeedsFallback starts the streakSTICKY BB answers → keep BNo timed return to AIF B FAILSTry C, then wrap to ASkip models already triedIf A answers again, the next default starts at A.A timer expiring is not a built-in transition.Explicit model selection still takes priority over the default route.
Stay, fail, wrap aroundA fallback success on B makes B the next default. B successes preserve the streak. If B fails, try C then A. When A answers, A becomes the next default. There is no timer-based expiry.Stay, fail, wrap aroundConceptual ordinary-request chain.PREFERRED AA fails → B succeedsFallback starts the streakSTICKY BB works → keep using BNo expiry timerIF B FAILSTry C, then wrap to AA succeeds → next start is ASkip candidates already triedAn explicit selection goes first.Do not invent an automaticfive-minute primary probe.
Figure 3. Conceptual transition map. Explicit selections and suspended-response continuations have their own precedence; use the documented contract for those cases. Original diagram: Tech Trend Insight. Source context: merged sticky-fallback proposal.

The merged proposal explicitly has no expiry timer. A healthy primary may therefore remain unused while B continues to work. If the product needs to test A every five minutes, that is a separate recovery policy. Define who performs the probe, how much traffic it receives and how to avoid sending the same user action twice.

Explicit model choices still take precedence over the default: a per-run model=..., Agent.override or SelectModel can select the first attempt. An explicit answer alone does not initiate stickiness. Test overrides in the installed version, especially when the explicit choice equals the default or falls outside the fallback chain. Suspended-response continuations also have special rules; do not treat them as fresh ordinary requests.

Use a local test before attaching real providers

The following original example uses FunctionModel, which runs a supplied Python function in place of a remote model. It simulates one unavailable primary and one working backup, then serializes history and recreates the agent for the next turn. The testing guide documents this testing mechanism. No key for a remote application programming interface (API) or paid inference call is needed.

from pydantic_ai import Agent, ModelMessagesTypeAdapter, ModelResponse, TextPart, models
from pydantic_ai.capabilities import ModelFallback
from pydantic_ai.exceptions import ModelAPIError
from pydantic_ai.models.function import AgentInfo, FunctionModel

models.ALLOW_MODEL_REQUESTS = False  # FunctionModel runs locally.
calls = []
health = {"primary": False, "backup": True}

def response(name):
    def answer(messages, info: AgentInfo):
        calls.append(name)
        if not health[name]:
            raise ModelAPIError(model_name=name, message="Simulated outage")
        return ModelResponse(parts=[TextPart(f"Local reply from {name}")])
    return FunctionModel(answer, model_name=name)

def new_agent(sticky=True):
    return Agent(response("primary"), capabilities=[
        ModelFallback(response("backup"), sticky=sticky)
    ])

first = new_agent().run_sync("Draft a short reply.")
assert calls == ["primary", "backup"]
encoded = ModelMessagesTypeAdapter.dump_json(first.all_messages())
restored = ModelMessagesTypeAdapter.validate_json(encoded)
calls.clear()
second = new_agent().run_sync("Make it shorter.", message_history=restored)
assert calls == ["backup"]
assert second.response.model_id is not None
print("Restored history:", calls, second.response.model_id)

With version 2.64.0, the local check produced ['primary', 'backup'] on the first turn and ['backup'] after restoration. Additional local checks made B fail while A recovered, observed ['backup', 'primary'], then observed ['primary'] on the following turn. A fresh conversation and a configuration with stickiness disabled both started on A again.

These are deterministic routing checks using local functions, not provider reliability or latency measurements. They establish that the example preserves and reuses routing history. Real-provider tests must additionally verify the output schema, tool behavior, model identifiers, privacy requirements and supported context size for every candidate.

The first stream event is the failover boundary

Streaming delivers a response through a sequence of events instead of waiting for one complete answer. ModelFallback waits for a candidate’s first event before handing its stream to the caller. An eligible failure before that handover can move to another candidate. A later stream failure is raised to the caller rather than silently joining two models’ answers.

Read Figure 4: Use the first event as the boundary. It may occur before the first readable word, so a user interface with no displayed text is not enough evidence that transparent failover remains possible.

Streaming changes which failures can be hiddenBefore the first stream event is handed over a failed candidate may fall back. After handover a later failure reaches the caller. The first event is not necessarily visible text.Streaming changes which failures can be hiddenConceptual event sequence; no latency scale.THE BOUNDARY IS THE FIRST STREAM EVENTFirst event handed to callerFailure before boundary → try BFailure after boundary → raiseNo partial A answer handed overCaller handles partial resultAn event can precede the first readable word.Restarting a whole application job needs its own duplicate-action policy.
Respect the stream boundaryBefore the first stream event is handed over a failed candidate may fall back. After handover a later failure reaches the caller. The first event is not necessarily visible text.Respect the stream boundaryConceptual event sequence.BEFORE FIRST STREAM EVENTCANDIDATE FAILSThe fallback may try BNo partial A stream handed overFIRST EVENT HANDED OVERLATER FAILURERaise to the callerHandle the partial resultThe first event need not bethe first readable word.Do not silently join an A prefixto a new B answer.
Figure 4. Conceptual streaming boundary from the documented first-event rule. Partial-result recovery and duplicate-action protection belong to the application. Original diagram: Tech Trend Insight. Source context: ModelFallback documentation.

For a partial support reply, the application can mark the response interrupted and offer a restart. If the run also created a ticket, restarting the whole job needs duplicate-action protection. An idempotency key identifies one intended action so repeated delivery can be recognized. Retrying a language-model response does not automatically make a database write idempotent.

Keep provider failure and answer rejection distinct. The default fallback condition covers ModelAPIError, an API failure reported by a model. A custom response handler can reject a successful response under an application rule. If you supply only a response handler as fallback_on, it replaces the default API-error condition; include both when both are intended. A rejected generated answer may still have consumed billable tokens.

Budget all attempts before calling failover “fast”

An API is the interface used to request a model response. A provider’s software development kit (SDK) may retry below the agent. The transport that sends Hypertext Transfer Protocol (HTTP) requests can retry below that SDK. Meanwhile, output correction can trigger another logical model request, and a durable workflow engine can restart an outer activity. These are different scopes, as the retry guide explains.

For a worked upper bound, assume two logical request steps, three model candidates per step including A, two SDK attempts for each model call and two transport attempts per SDK attempt. If every eligible budget is exercised, the bound is 2 × 3 × 2 × 2 = 24 wire attempts. Each two-attempt budget includes its initial attempt and one retry.

Read Figure 5: Multiply the explicitly bounded layers. Do not read “two retries” as “two attempts,” and include outer workflow re-entry separately.

Nested recovery can multiply the workTwo logical model steps, three candidates per step, two SDK attempts and two transport attempts can yield 24 wire attempts if all budgets are exercised. Outer durable restarts add another factor.Nested recovery can multiply the workWorked attempt-budget example. No provider defaults or measured timings are assumed.LOGICAL REQUESTSN = 2 model stepsOne bounded runCANDIDATES PER STEPF = 3, including AA, B and CINNER ATTEMPTSM = 2 SDK attemptsK = 2 transport attempts2 × 3 × 2 × 2 = up to 24 wire attemptsEach “2 attempts” means initial try + one retry.This is an assumed upper-bound scenario, not a measured trace.An outer workflow restart can multiply the work again.
Budget attempts, not labelsTwo logical model steps, three candidates per step, two SDK attempts and two transport attempts can yield 24 wire attempts if all budgets are exercised. Outer durable restarts add another factor.Budget attempts, not labelsWorked recovery-budget example.OUTER WORKN = 2 logical model stepsF = 3 candidates per stepINNER WORKM = 2 SDK attemptsK = 2 transport attempts2 × 3 × 2 × 2 = up to 24wire attemptsAssumed upper bound.Attempts include the initial try.Outer restarts add another layer.
Figure 5. Worked maximum for the explicitly bounded example. Actual attempts depend on error eligibility, successful stops, deadlines and the provider implementation. Original diagram: Tech Trend Insight. Source context: retry guide.

This bound is a planning example, not a forecast. Success can stop the chain early; some errors are ineligible; a deadline can interrupt it. If the number of logical steps is unbounded, the four-factor calculation supplies no overall bound. Record actual candidate attempts, elapsed waits, corrections and any outer restarts.

The retry guide recommends choosing a retry owner. Passing model_settings={'max_retries': 0} disables provider-SDK retries where that setting is supported; support varies by provider. It does not remove the first request, network timeout, transport retries, validation corrections or durable retries. Nor does it make failover instantaneous. Confirm the settings on each candidate rather than copying a universal zero-retry prescription.

Prompt-cache continuity is an opportunity, not a promise

A prompt cache reuses qualifying previously processed input at the provider, as the caching guide explains. Remaining on one provider/model can help preserve a reusable prefix and reduce unnecessary switching. But changing models does not necessarily delete a cache, and returning to B does not guarantee a hit. Prefix contents, cache lifetime, model identity and provider policy still matter.

Measure cached input and billed usage in the actual provider records before claiming savings. Log failed-attempt usage when available, but do not turn missing usage into zero cost. The same trace should distinguish an API failure with no usable response from a generated response rejected by policy; their observable work and charges can differ.

Decide whether you need conversation or fleet recovery

A circuit breaker is a shared control that limits requests to a service judged unhealthy and can allow controlled recovery probes. Sticky conversation routing has a smaller scope. Conversation X can stay on B while a new conversation Y still starts on A. A global outage view is not implied by X’s saved history.

Read Figure 6: Compare the two conversations before looking at the fleet policy. Sharing a deployment does not make their routing histories shared state.

Conversation routing and fleet health have different scopesConversation X retains a fallback to B while a new conversation Y still starts on A. Shared outage state and timed primary probes require a separate fleet-level policy.Conversation routing and fleet health have different scopesConceptual responsibility map, not a gateway product specification.CONVERSATION XHistory shows fallback to BNext default starts on BNEW CONVERSATION YNo previous fallback recordIts default still starts on AFLEET HEALTH POLICYShared outage state across usersTimed probe of primary AGradual traffic returnSeparate application or gateway logicPer-conversation stickiness does not coordinate the whole fleet.Decide when you need both scopes and how they interact.
Two different routing scopesConversation X retains a fallback to B while a new conversation Y still starts on A. Shared outage state and timed primary probes require a separate fleet-level policy.Two different routing scopesConceptual responsibility map.CONVERSATION XFallback recorded: BNext default: BNEW CONVERSATION YNo fallback historyDefault still starts on AFLEET HEALTH POLICYShared outage stateTimed primary probeSeparate routing controlChoose the scope deliberately.Sticky alone does not supply both.
Figure 6. Conceptual scope comparison. A circuit breaker coordinates whether requests should reach an unhealthy service; sticky fallback chooses a default from one conversation’s evidence. Original diagram: Tech Trend Insight. Source context: merged sticky-fallback proposal.
RequirementUseful controlEvidence to retain
Continue a chat on its working backupSticky fallback plus preserved typed historyServing model ID, prior failures and restored-history test.
Reduce requests to an unhealthy provider across usersShared service-health routing or circuit breakerHealth signal, scope, probe policy and traffic-return rule.
Bound retries and waitingAttempt budgets at each layer and an application deadlineActual attempt counts, waits and terminal result.
Accept a backup’s output or tool requestSchema validation, task checks and separate authorizationValidated result, reviewer/policy decision and action record.

For rollout, choose a bounded draft-only task first. Exercise an unavailable A, a working B, a failed B with recovered A, an explicit override, a newly created conversation, a history round-trip and a stream failure after the first event. Verify the application-visible outcome as well as which model was called.

My adoption rule is to enable stickiness when continued use of an acceptable backup is preferable to repeated primary failures within that conversation. Keep the backup subject to the same task and action constraints. A working route is valuable evidence about execution; it is not a grant of authority or proof that the generated answer is right.

Sources and scope: Official Pydantic documentation, merged PR and package record checked November 2, 2026. Routing checks used local FunctionModel responses with pydantic-ai-slim 2.64.0 and Python 3.14.3. Diagrams, the support-chat scenario, retry arithmetic and rollout criteria are original explanatory material; no real provider outage, prompt-cache benchmark or paid model call was used.

Popular posts from this blog

OpenAI Prompt-to-Voice Architecture: From Voice Description to Live GPT-Live Sessions

Whenever I help teams deploy customer-facing voice assistants, we prioritize acoustic clarity and barge-in reliability over theatrical vocal persona. OpenAI’s prompt-to-voice feature makes generating a custom vocal timbre effortless, but production readiness still depends on strict turn-taking rules, verified tool execution, and audio retention controls. OpenAI’s October 16 release notes announced text-described voices for API customers. This guide explains how a description becomes reusable configuration, how to connect it to GPT-Live, and what to check before using it with callers. A fictional help desk provides the examples; the emphasis is on understanding the workflow rather than memorizing acoustic terminology.

Meta’s VideoJAM Explained: Why Motion Coherence Matters in AI Video

VideoJAM is easy to misunderstand because its name sounds like a product. The paper presents something narrower and more useful to analyze: a framework that gives a video generator an explicit motion prior. It is designed to address temporal coherence—the way objects, bodies, and physical interactions should remain plausible as a clip unfolds—not to provide a consumer dashboard with templates or a “generate video” button. Key Takeaways VideoJAM is a research framework for video generators, not a consumer-facing app with templates or a standalone UI. The training objective predicts appearance and motion from one joint representation; Inner-Guidance uses the model’s evolving motion prediction during sampling. On VideoJAM-bench, the authors reported motion-preference results against Sora, Kling 1.5, and Runway Gen3, while visual quality and motion remain separate measures. The experiments used 256×256, 128-frame clips at 24 fps and a motion-focused 128-prompt benchmark, so the results do ...

Grok 3’s 2025 Release: What xAI Announced, What Arrived, and What Changed

“When will Grok 3 launch?” was a moving target in February 2025. Elon Musk and xAI teased the model before the public beta announcement, so headlines often mixed a promised date with a product that was still being staged. The durable story is the timeline: a training claim, an announcement, limited access, and a later evaluation period are four different events. Key Takeaways xAI’s written Grok 3 announcement is dated February 19, 2025. That document date alone does not establish the time of a livestream or first access for every account. The beta included Grok 3, Grok 3 mini, optional Think modes, and the DeepSearch research agent. Early access was tied to Grok.com and X Premium tiers; xAI said API access was coming later at announcement time. A release date is not a quality guarantee. Check availability, rate limits, citations, and tool permissions in the environment you actually use. Open original image at full size ↗ Figure 1: Official NVIDIA–xAI visual from the October 2024 Coloss...