Timeouts
Bounding how long one step inside a run may take, bounding the whole run, and ending a run from inside a tool are answered by separate mechanisms with separate failure modes. This page maps them. To stop a run that is already in flight, see Cancelling a Run.
Each knob below bounds a different unit of work.
| What you want to bound | How to set it | What happens on expiry |
|---|---|---|
| A single model request attempt — a provider SDK client’s retries re-arm it for every attempt | timeout on ModelSettings | ModelTimeoutError, a ModelAPIError, on the model classes that map transport errors; the run fails unless a FallbackModel or a transport retry handles it |
| A whole model request — every attempt the provider SDK and transport make, and reading a streamed response through its last chunk | request_timeout on ModelSettings — see Model request deadlines | ModelRequestTimeout, a ModelAPIError; the run fails unless a FallbackModel falls back on it |
| A function tool call | Agent(tool_timeout=...), or timeout= on an individual tool — see Tool Timeout | The model receives a retried tool result 'Timed out after N seconds.', consuming that tool’s retry budget. A def tool is not actually stopped: the deadline is enforced around the await, so the worker thread runs to completion |
| A hook function | timeout= on the @hooks.on.* decorator | HookTimeoutError, which is an AgentRunError and aborts the run. Like a def tool, a def hook is not actually stopped: the worker thread runs to completion |
| Connecting to an MCP server | MCPToolset(init_timeout=...), default 5 seconds | The connection and initialize handshake fail |
| A single MCP request | MCPToolset(read_timeout=...), default 300 seconds | The request fails; under the default tool_error_behavior='retry' the model sees it as a retryable tool error |
| Opening a realtime session | handshake_timeout on RealtimeModelSettings, default 30 seconds — OpenAI, Azure OpenAI, xAI, and Gemini | Opening the session raises RealtimeError. On a reconnect it consumes a ReconnectPolicy attempt instead |
| Total work done by a run | UsageLimits — requests, tool calls, tokens, or cost — see Usage Limits | UsageLimitExceeded |
| Wall-clock duration of a whole run | timeout= on any run method — see Bounding a whole run | RunTimedOut, carrying everything the run completed |
Three of these need qualifying:
-
ModelSettings['timeout']is applied per model class, not universally. The model classes that forward it to their provider client are listed underModelSettings.timeout; the ones built on OpenAI’s inherit the forwarding fromOpenAIChatModel/OpenAIResponsesModel. Other model classes ignore the setting, warning withUnsupportedSettingWarning, and the timeout on the HTTP client they were built with applies instead. When Pydantic AI creates that client itself, it defaults to a 600-second total timeout (20 minutes for Google) with a 5-second connect timeout. Google and Mistral additionally reject anhttpx.Timeoutobject and accept only a number of seconds.To bound a request on a model class that ignores the setting, configure the timeout where that provider actually takes one. Most providers accept your own
http_client, but several don’t:XaiProvidertakes a client-leveltimeout(or a preconfiguredxai_client),BedrockProvidertakesaws_read_timeoutandaws_connect_timeout(or a preconfiguredbedrock_client), andHuggingFaceProviderrejectshttp_clientoutright in favor ofhf_client.On a client Pydantic AI created, including one from
create_async_httpx2_client(), a request timeout given in seconds can shorten but never lengthen the client’s connect timeout (5 seconds by default) and pool timeout (600 seconds by default, 20 minutes for Google). This includes the timeout google-genai sends with every Gemini request, which equals the client’s read timeout. To connect for longer, pass anhttpx.Timeoutwhoseconnectdiffers from its other phases, or your ownhttp_client. -
ModelSettings['request_timeout']is enforced by Pydantic AI, on every model. It is never sent to the provider, so it works the same everywhere, including on the model classes that ignoretimeout. See Model request deadlines. -
Tool timeouts are enforced by
FunctionToolsetonly, and each toolset carries its own.Agent(tool_timeout=...)sets the default for tools you register on the agent — it does not reach into aFunctionToolsetyou constructed yourself and passed viatoolsets=[...]. Give that toolset its ownFunctionToolset(timeout=...), or settimeout=on the individual tools. Tools coming from an MCP server, an external toolset, or a customAbstractToolsetread neither; bound those with the server-side or transport-level timeout instead.
If you enforce a deadline inside a tool body yourself, catch the TimeoutError and re-raise it as ModelRetry or ToolFailed rather than letting it escape. What happens to a bare TimeoutError depends on whether that tool has a timeout of its own:
- No
timeouton the tool or its toolset. It is an ordinary exception and propagates out of the agent run — unless a capability implementson_tool_execute_error, which can turn it into a replacement tool result or aModelRetry. - A
timeoutis configured. The call runs insideanyio.fail_after(timeout), which signals expiry withTimeoutErrortoo, so aTimeoutErroryou raised yourself is indistinguishable from the deadline expiring and becomes the same'Timed out after N seconds.'retried tool result — reporting a deadline that may never have passed.
Re-raising in the tool is the more local choice; the hook is for applying one policy across every tool.
[ModelSettings['timeout']][pydantic_ai.settings.ModelSettings.timeout] is applied by the provider client to one attempt at a request: each SDK retry gets a fresh one, and for a streamed response it only limits the wait for each chunk, not the whole response. request_timeout is one clock over the whole request to one model, enforced by Pydantic AI: every attempt the provider SDK and HTTP transport make, the backoff between them, any continuation of a response the provider paused, and reading a streamed response through its last chunk. In Temporal’s terms, timeout is the start-to-close timeout of one attempt, and request_timeout is the schedule-to-close timeout of the request.
from pydantic_ai import Agent, ModelRequestTimeout
agent = Agent(
'openai:gpt-6.1-sol',
model_settings={
'timeout': 30, # (1)
'request_timeout': 90, # (2)
},
)
async def main():
try:
result = await agent.run('What is the capital of France?')
print(result.output)
#> The capital of France is Paris.
except ModelRequestTimeout as e:
print(f'{e.model_name} took longer than {e.timeout} seconds') Each attempt gets 30 seconds, and the OpenAI SDK retries a failed attempt twice by default.
However many attempts it takes, the request ends after 90 seconds.
When the deadline passes, the request is cancelled and ModelRequestTimeout is raised. It’s a ModelAPIError, so an on_model_request_error hook can recover from a timeout of a non-streamed request, like from any other failed request. A timeout of a single attempt, raised by the provider client or transport and controlled by timeout, is a different error and is never reported as ModelRequestTimeout.
- Streaming. The deadline is fixed when the request is made and covers reading the stream until its last chunk, so the time your code takes to consume the events counts towards it: a consumer that is still reading when the deadline passes gets
ModelRequestTimeoutfrom the stream. What has been streamed so far is kept in the message history as an'interrupted'response. Once the last chunk has been read, the deadline no longer applies. This includes the streamingrun()does behind the scenes, for example with an event stream handler. - Fallback. A
FallbackModelgives each model it tries a fresh deadline, and its defaultfallback_onmoves on to the next model when one runs out. Each attempt that ran out is recorded in the response’sfailed_attempts. If the model it picked pauses its response, the continuation is still the same request to that model and keeps its deadline, while a later run that resumes the paused response from message history starts a fresh one. A model’s ownsettingscan set a differentrequest_timeoutfor it. Once a streamed response has started,FallbackModelno longer falls back, so running out while it’s being read raisesModelRequestTimeout. - Durable execution. On Temporal, the deadline runs on the workflow’s clock, as a durable timer. When it passes, the model request activity is cancelled, which also ends its activity retries, and
ModelRequestTimeoutis raised in the workflow. AFallbackModelruns inside the activity, along with the deadlines it gives each model. Each segment of a paused response is a separate activity, and the deadline of the model it picked carries over from one to the next as a wall-clock timestamp, so it still covers the whole continuation. On DBOS and Prefect, aFallbackModel’s deadlines carry over between steps the same way. Otherwise the deadline runs on the process’s clock, around the model request step, and a workflow that is recovered after a crash starts a fresh one. - Run timeouts.
timeout=on a run method bounds the whole run, including its model requests, so both deadlines apply to a request and the earlier one wins. If the run’s deadline passes first, the request is cancelled along with the rest of the run, which raisesRunTimedOutrather thanModelRequestTimeout, and aFallbackModeldoesn’t fall back. - Direct requests.
model_request()andmodel_request_stream()apply it too. - Batching. Under
BatchModelRequests, the deadline covers the whole wait, from when the request is queued until its batch result arrives.model_request_batch()refuses it.
Pass timeout= (in seconds) to run(), run_sync(), run_stream(), run_stream_sync(), run_stream_events() or iter() to bound everything the run does: every model request, tool call and hook, and, for the streaming methods, the time you spend consuming the stream. When the time is up, the run is cancelled the same way as with CancellationToken: the in-flight model request or stream is closed, running tool calls are cancelled, and the run raises RunTimedOut with everything it completed:
from pydantic_ai import Agent, RunTimedOut
agent = Agent('openai:gpt-6.1-sol')
async def main():
try:
result = await agent.run('What is the capital of France?', timeout=30)
except RunTimedOut as exc:
history = exc.all_messages() # (1)
print(f'Timed out with {len(history)} messages to resume from')
return
print(result.output)
#> The capital of France is Paris. all_messages() includes the completed tool results and the partial response of an interrupted stream. Pass it as message_history to a new run to resume.
RunTimedOut subclasses RunCancelled, so except RunCancelled handles every way a run can be stopped on purpose. It also subclasses TimeoutError, the error asyncio.timeout() raises. Unlike asyncio.timeout(), it doesn’t need the run’s state recovered from the exception chain, and the deadline is visible inside the run:
RunContext.deadlineis the time the run has to finish by, in UTC, andRunContext.remaining_time()returns the seconds left, so a tool can size its own work or timeouts to fit.- An agent run from a tool, as in agent delegation, inherits the deadline. Its own
timeout=can make it earlier, but not later. When the shared deadline passes, the parent raisesRunTimedOut; a sub-agent’s own, shorter timeout fails just the delegate tool, like a sub-agent cancelling itself. - Running out of time is cancellation, not a model error, so a
FallbackModeldoesn’t fall back on it. To give up on one slow model request and fall back while the run goes on, userequest_timeouttoo: whichever deadline passes first applies. - The deadline is enforced with a timer on the asyncio event loop, so under another async backend, such as Trio,
timeout=raises aUserErrorbefore the run starts.
from pydantic_ai import Agent, RunContext
research_agent = Agent('openai:gpt-6.1-sol')
agent = Agent('openai:gpt-6.1-sol')
@agent.tool
async def research(ctx: RunContext, topic: str) -> str:
remaining = ctx.remaining_time()
if remaining is not None and remaining < 10:
return 'Not enough time left to research this topic.'
result = await research_agent.run(topic) # (1)
return result.output The sub-agent run stops at the same deadline as the run that called this tool.
The UI adapters’ run_stream(), run_stream_native() and dispatch_request() take timeout= too. run_stream() and dispatch_request() report a run that runs out of time to the client the same way as a first-party cancellation (see Vercel AI and AG-UI), and pass the RunTimedOut to their on_cancel callback. run_stream_native() raises it.
Under durable execution, the deadline is stored as wall-clock time, so a run that’s replayed or recovered keeps the deadline it started with. The TemporalDurability, DBOSDurability and PrefectDurability capabilities take care of this; the deprecated wrapper agents reject timeout=. See the notes for Temporal, DBOS and Prefect.
What a tool raises decides whether the run continues, and what the model gets to see:
| Raise | Run continues? | The model sees |
|---|---|---|
ModelRetry | Yes | A retried tool result asking it to correct the call — consumes that tool’s retry budget |
ToolFailed | Yes | A failed tool result to adapt to — does not consume the retry budget |
ApprovalRequired / CallDeferred | Ends the run with a DeferredToolRequests output, unless a HandleDeferredToolCalls handler resolves the call inline | Nothing yet — see Deferred Tools |
| Any other exception | No | By default nothing — it propagates out of agent.run(). A capability implementing on_tool_execute_error sees it first and can return a replacement tool result or raise ModelRetry, letting the run continue |
The deferred row reads differently inside a realtime session, which has no way to pause: a live conversation can’t wait for an out-of-band result. A HandleDeferredToolCalls handler still gets the chance to resolve the call inline, but where a run would end with a DeferredToolRequests output, a session instead answers the model with an explanation that the tool can’t complete during the session, and keeps going. See Deferred and approval-required tools. The last row reads differently there too: a session answers the model with a generic failure, never the exception’s message, and keeps going, while the exception is still raised to you. See Function tools.
A tool can also end the run without raising, by calling RunContext.cancel() — the run ends with RunCancelled and the tool’s return value is discarded. See Cancelling the Run from a Tool.
There is no exception that ends a run early with a successful output. To let a tool finish the run with a value, make that value the run’s output: give the agent an output tool the model can call, or an output function that produces the result.