Skip to content

Connection lifecycle

A realtime model uses one persistent provider connection. Your backend owns that session and the media bridge to the user (see Connecting a frontend); a reconnect policy can recover dropped connections and provider session limits without changing the application event loop.

The session lifecycle

stateDiagram-v2
    [*] --> Connecting: session() opens
    Connecting --> Listening: handshake complete
    Listening --> UserTurn: speech detected /<br>audio committed
    UserTurn --> ModelResponse: turn detection /<br>create_response()
    ModelResponse --> ToolCalls: model calls a tool
    ToolCalls --> ModelResponse: result returned
    ModelResponse --> Listening: turn complete
    Listening --> Reconnecting: connection drops
    ModelResponse --> Reconnecting: connection drops
    Reconnecting --> Listening: redial succeeds
    Reconnecting --> [*]: attempts exhausted
    Listening --> [*]: close()

Opening the session performs the provider handshake, after which the session listens for input. Turn detection (or manual push-to-talk control) moves a user turn into a model response, which may loop through tool calls before RealtimeTurnCompleteEvent marks the turn boundary and the session listens again. A dropped connection enters the reconnect loop below — emitting RealtimeSessionReconnectEvent on recovery — until close() (or leaving the async with block) ends the session.

Connection and handshake

The connection is opened when the session() context is entered, and the shared handshake_timeout setting (default 30 seconds) bounds how long the session waits for each realtime protocol handshake event on providers with an explicit handshake (OpenAI, Azure OpenAI, and xAI). A handshake that times out raises RealtimeError; a rejected WebSocket upgrade raises ModelHTTPError (see Errors).

Reconnecting

Set the reconnect shared setting to a ReconnectPolicy to redial with exponential backoff, reapply configuration, and emit RealtimeSessionReconnectEvent. Like any realtime model setting, it can be a default on the model or passed for one session:

from pydantic_ai import Agent

agent = Agent()
realtime = agent.realtime(
    'openai:gpt-realtime',
    model_settings={'reconnect': {'max_attempts': 5}},
)

max_attempts bounds retries for one drop. max_reconnects bounds recoveries across the entire session, preventing an endpoint that repeatedly accepts and closes connections from redialing forever.

Without a policy, an unexpected provider close raises RealtimeError from the session iterator.

On a WebRTC sideband the same policy applies to an unexpected drop, but a clean close is treated as the browser hanging up: the sideband is a control channel, so a normal close ends iteration without a session error or reconnect attempt even when a reconnect policy is set. The close frame alone can’t distinguish a hangup from a WebSocket-terminating proxy closing the sideband cleanly mid-call (a restart or graceful rotation), which would end the agent side while the browser keeps talking to the provider — drain such connections at the infrastructure layer rather than relying on the reconnect policy to cover them.

State restoration

OpenAI and Azure OpenAI have no cross-connection server state, so Pydantic AI replays local message history into the new session. Prior transcript turns survive; in-flight audio does not.

Gemini and xAI use native in-process session resumption, enabled automatically when a reconnect policy is present (an explicit google_enable_session_resumption=False alongside a policy raises UserError instead of silently losing the conversation); see the Gemini resumption settings. Their handles live only in memory and cannot be persisted for another process.

RealtimeSessionReconnectEvent.state_restored reports whether the reconnect carried the conversation through without cutting a turn off.

How a reply the drop caught in flight is handled depends on the mechanism. Under native resumption (xAI) the recorded response simply stays open: output on the new connection continues it, the turn completes with the response terminal as usual, and state_restored stays True. Gemini also reports True but closes the cut reply as an interrupted response (keeping any partial transcript in history) before the RealtimeSessionReconnectEvent and stays quiet until the next input.

Local replay (OpenAI, Azure OpenAI) restores only the finalized turns, so a reply in flight when the socket dropped cannot continue. The session settles it before emitting the event — the partial reply becomes an interrupted response, running tool calls get cancelled returns, and the turn ends so queued messages waiting for the boundary still flush — and state_restored is False to say the turn was cut off. An answer that was solicited but had not started streaming is instead re-requested on the new connection, and a drop with nothing in flight restores the whole call as-is; both lose no output, so state_restored stays True.

Provider session limits

Providers cap individual connection duration. A reconnect policy is also how an application survives those limits. Exact limits and provider behavior can change, so provider pages are canonical:

Gemini sends GoAway shortly before its cap but Pydantic AI currently reconnects only after the connection drops, so a long call can briefly drop mid-turn.

Errors

Realtime sessions use the standard Pydantic AI exception hierarchy:

ExceptionRaised when
UserErrorThe application requests an unsupported operation, passes incompatible settings, lacks credentials, or misuses the session.
ModelHTTPErrorThe provider rejects the WebSocket upgrade with an HTTP status.
RealtimeErrorThe connection fails, times out, closes unexpectedly, returns an invalid frame, or exhausts reconnect attempts.
UsageLimitExceededA configured usage limit is exceeded.

RealtimeError subclasses ModelAPIError, so except ModelAPIError covers HTTP and non-HTTP provider failures together.

Recoverable failures arrive as events: RealtimeSessionErrorEvent for provider operations and RealtimeInputTranscriptionErrorEvent for one failed user transcription. The session remains usable after either event.

Failures surface from the responsible call where possible; a failed send_audio() raises there. Receive-loop and tool failures propagate from session iteration.

For symptom-first debugging, see Troubleshooting.