# Bounded by design: timeouts, tool caps, request limits and one run per thread

> The limits a 10xGraph run has by default: parallel tool cap, run, node and tool timeouts, per-user run cap, one run per thread, request size limits and paging caps.

Source: https://10xgraph.com/blog/bounded-runs
Last updated: 2026-11-10

Agents are loops that call a model and a set of tools, and nobody wrote down in advance how long the loop will take or how many tools one turn will start. A hung tool, a model that emits hundreds of calls in one message, two tabs sending to the same thread and a client that posts a huge body are all ordinary events once real users arrive. If nothing bounds them, the first one that happens takes the worker down.

10xGraph is open-source, self-hosted infrastructure for AI applications. You write the agents, tools and tasks; 10xGraph runs them behind an API server with authentication, role-based access, tenant isolation, durable state in Postgres and Redis, background jobs, metrics and tracing, and generates Docker and Kubernetes deploy files. MIT licensed, by 10xScale. Formerly Agentflow.

This post lists the bounds that 10xGraph and `10xgraph-api` apply by default, with the numbers from the code. They are a backstop against runaway work, not a latency budget. The operating guidance is on the [scaling page](/docs/production/scaling).

## Parallel tool calls: at most 10 at a time

One model turn can ask for many tool calls, and the ToolNode runs them in parallel. Each parallel branch holds a deep copy of the state, so memory grows with the call count. In a test of one turn with 300 tool calls, the unbounded version used 2.4 GB.

At most `max_parallel_tool_calls` run at once, and the default is 10. Set it in the run config, process-wide with the `TENXGRAPH_MAX_PARALLEL_TOOL_CALLS` environment variable, or, when you serve with the API, as `runs.max_parallel_tool_calls` in `10xgraph.json`. The API writes it into every run's config, so clients cannot raise it.

## Timeouts at three levels

| Bound | Default | Where it is set |
|---|---|---|
| Whole run | 300 s with `MODE=production`, no limit in development | `runs.timeout` in `10xgraph.json` |
| One node | No limit | `node_timeout` in the run config |
| One tool call | No limit | `tool_timeout` in the run config |

A run that passes `runs.timeout` is cancelled: invoke answers 504, and a stream, WebSocket run or AG-UI run ends with an error. Realtime sessions are not limited. An explicit number overrides the default and an explicit `null` removes the limit, in either mode.

There is no default node or tool limit: how long they may run is your call. A value you set applies to both invoke and stream. Set the node limit above the 600 s default timeout of the model SDKs, so a slow model call fails with its own error instead of being masked by the node deadline. `None` or `0` means no limit.

## One run per thread

Two runs writing the same thread would race on its state. The API takes a per-thread lock for invoke, stream and WebSocket runs. A request that finds the thread busy waits up to 30 seconds and then gets 409.

Send `on_busy` with the request to choose what a waiting request does:

- `"wait"` (the default) waits for the active run to finish.
- `"cancel"` first asks the active run to stop, and repeats the request every second while it waits, in case the run had not saved its state yet.

With a Redis `cache_store` the lock is a lease that a background task renews while the run is alive. If the worker dies, the lease expires on its own (60 seconds after the last renewal), so a crashed worker cannot hold a thread forever. With the default in-memory cache store the lock is per process.

## Per-user run cap

`runs.max_concurrent_per_user` caps the HTTP graph runs (invoke, stream, AG-UI) that one verified user can have in flight at once. The default is 10, and `0` or `null` means unlimited. Over the cap the request gets 429 before the response starts. Requests without a verified user, which means auth is off, are not capped. Like the thread lock, the count is shared across workers only when `cache_store` is `redis`.

## Request size limits

Limits on what a client can post are fixed. They have no setting, on purpose.

- Body size: `MAX_REQUEST_SIZE`, 10 MB by default. A declared `Content-Length` over the limit is rejected before any body is read, and a streamed body is cut off as soon as it passes the limit. The upload route has its own limit sized to `MEDIA_MAX_SIZE_MB` (25 by default). WebSocket frames on `/v1/graph/ws` use the same cap, and frames on the realtime route `/v1/graph/live` are capped at 1 MiB.
- Per request: at most 500 messages, 100 content blocks per message and 1,000,000 characters of text across all text blocks.
- AG-UI clients resend the whole conversation on every run. The trailing turn gets the limits above, and the full history gets a looser cap of 10,000 messages.

## WebSocket connections

The `websocket` block in `10xgraph.json` caps open connections: `max_connections` is 1000 for the server and `max_connections_per_user` is 10 for one verified user. Like the run cap, these counts are shared across workers only with a Redis `cache_store`.

## Paging and the in-memory cache store

Checkpointer list endpoints default to 100 rows, cap `limit` at 1000 whatever the client sends, and the core checkpointers lower an explicit `limit` above `MAX_LIST_LIMIT` (also 1000) and raise `ValueError` for a negative `limit` or `offset`. Passing no limit to the Python API still returns everything, which is for code you control, not for a client.

The in-memory cache store, which holds rate-limit counters and run caps when no Redis is configured, keeps at most 50,000 slot keys. At the cap it drops expired entries first, then evicts the least recently used key that is not protected, and logs a warning at most once a minute. Thread locks, run and WebSocket caps and token revocations are protected, so a flood of distinct client addresses cannot evict them. For more than one worker, set `cache_store` to `redis`.

## What is not bounded

Be clear about the gaps.

- Node and tool timeouts are off until you set them. Without them, `runs.timeout` is the only deadline on a run.
- Counts such as the per-user cap, the WebSocket caps and the thread lock are per process until `cache_store` is `redis`.
- These bounds protect the server. They do not cap what a model costs you. Set provider-side spend limits too.

## Frequently asked questions

### What is the default run timeout?

With MODE=production and no runs.timeout in 10xgraph.json, a run is cancelled after 300 seconds. In development there is no limit. An explicit number overrides the default, and an explicit null removes the limit in either mode.

### What happens when two requests use the same thread at once?

The second waits up to 30 seconds for the first to finish, then gets 409. With on_busy set to cancel, it asks the active run to stop first.

### Do these limits apply to streaming runs?

Yes. runs.timeout, the per-user run cap, the thread lock, the request size limits, and any node_timeout or tool_timeout you set apply to streaming runs too.
