Quick Start
1
Limit parallel runs of an agent
Control how many instances of the same agent can run concurrently:
2
Same, async
Use async context for better resource utilization:
3
Bound tool time with ToolConfig
Prevent slow tools from blocking agent execution:
How It Works
Retune the limit safely at any time
Callset_limit() at any time — even while agents already hold permits — and the new cap takes effect immediately without letting phantom permits leak.
PraisonAI’s concurrency controls at a glance:
ConcurrencyRegistrycaps how many agent runs happen in parallel.- Per-agent tool executor applies
tool_timeoutand recycles a hung worker. AgentTeam/PraisonAIAgentsinstances are single-run — a second concurrentstart()/astart()on the same object raisesRuntimeError. Batched runs viastart_for_each/astart_for_eachare unaffected. See AgentTeam Batch Runs → Concurrent-run safety.
Global cap across loops and threads, and safe to retune live. Each agent’s limit is backed by a loop-neutral, in-place-resizable limiter keyed by agent name, so the configured cap holds across every event loop and thread — and calling
set_limit() while agents are already running retunes the limit safely for in-flight holders without ever exceeding the new cap. Sync and async callers share the same per-agent permit pool. See PR #5050 (loop-neutral) and PR #5330 (safe retune).Dynamic reconfiguration safety
Since PR #5258,set_limit() and remove_limit() are safe to call while permits are still outstanding. Each acquire() / acquire_sync() now returns the exact semaphore it obtained, and the matching release() frees that same instance — so a runtime cap change never redirects a release onto a fresh semaphore and never silently inflates the effective cap.
Before the fix, calling set_limit("agent", N) or remove_limit("agent") while a permit was outstanding could permanently corrupt the cap: the outstanding release landed on a newer semaphore, which threading.Semaphore (unlike BoundedSemaphore) accepts without raising. From that point on the configured cap was meaningless and the drift was undetectable from the public API.
release(name) remains backward-compatible: passing only a name still works via a name-lookup fallback. The exact-handle path is used automatically when you release the pair returned by acquire() / acquire_sync().Sync vs Async Rule
acquire_sync() is safe from any context; in async code prefer await acquire() to avoid blocking the loop.
Prefer
await acquire() in async code:
Tool Timeout Behavior
Whentool_config=ToolConfig(timeout=...) is set, tools run in a dedicated executor with these characteristics:
In YAML the field name is still tool_timeout:; in Python use tool_config=ToolConfig(timeout=…).
Timeout Return Shape
On timeout, each layer surfaces the timeout differently:Effective Timeout Precedence
Whentool_timeout values are declared, the wrapper resolves each agent’s budget independently:
- CLI wins for every agent. An explicit
--tool-timeout Non the command line (orcli_config={"tool_timeout": N}when embedding) is used verbatim for all agents. - Uniform declared values take the shared-wrap fast path. When every agent under
roles:andagents:declarestool_timeoutand every declared value is identical, a single guard wraps the shared tool dict. If any agent omits the field, the shared wrap is skipped and the per-agent resolver runs instead. - Heterogeneous per-agent values are honoured per agent. When agents declare different values, each agent’s tools carry its own budget (tool objects stay shared; only the guard closure differs). The tightest value no longer collapses onto every agent. Agents that omit
tool_timeoutget no wrap (they do not inherit another agent’s declared value). - Otherwise, no wrapping. If nothing declares a timeout, tools run without wrapper-layer enforcement (the SDK executor-layer enforcement still applies if
tool_config=ToolConfig(timeout=…)is set in Python).
AgentsGenerator._resolve_uniform_tool_timeout(config); heterogeneous budgets use make_agent_tool_wrap_resolver(config) / resolve_agent_tool_timeout(agent_key, config) — see praisonai/agents_generator.py.
The timeout pool size (not the per-tool budget above) is per-run configurable via tool_timeout_workers on AgentsGenerator (or cli_config["tool_timeout_workers"]), falling back to PRAISONAI_TOOL_TIMEOUT_WORKERS (default 32). See Tool Configuration.
As of PR #5228, the generator no longer stamps its internal
_tool_timeout_wrap / _agent_tool_wrap_resolver closures onto a cli_config dict you pass in. Those are private per-run closures kept on AgentsGenerator._run_ctx and merged into the adapter’s cli_config kwarg only at dispatch (_dispatch_cli_config()). Multi-tenant embedders can safely reuse a single cli_config template across runs — it will not come back holding instance-bound lambdas from a prior run.Executor Details
- One executor per
Agentinstance (lazy creation) max_workers=2threads per agent- Thread name prefix:
tool-<agent_name>— useful for log filtering - Reused across calls — no resource leak
- Recycled on timeout — a tool that hangs past
tool_timeoutis not reclaimable, so the executor isshutdown(wait=False)and the next call gets a fresh worker
Self-healing after a hang. The pool has 2 workers by default; before PR #3960, two consecutive hangs would deadlock the pool because
future.cancel() cannot stop a thread that has already started. Now the executor is recycled on timeout — the next call starts on a fresh worker — so a hung tool cannot progressively degrade throughput toward a deadlock. Recycling is bounded so repeated timeouts cannot leak an unbounded number of stuck threads.Common Patterns
Limit FastAPI Route Concurrency
registry.set_limit("chat_agent", N) again on a running server — a config reload or hot re-tune — is now correct even with requests in flight. Each in-flight request releases the exact semaphore it acquired, so the new cap takes effect for subsequent acquires without the old outstanding releases inflating it (PR #5258).
Async Context Manager Helper
Timeout Selection by Tool Type
Best Practices
Always release in finally blocks
Always release in finally blocks
Prevents deadlocks when exceptions occur:
Prefer async acquire in async code
Prefer async acquire in async code
Mixing is no longer an error —
acquire_sync() in an async context simply blocks its thread. Sync and async callers contend for the same per-agent permit pool (see the test_sync_and_async_share_one_pool test in PR #5050). Still prefer await acquire() in async code so the event loop keeps running:Set tool_timeout for network tools
Set tool_timeout for network tools
Any tool that does network IO should have a timeout:
Use thread names for debugging
Use thread names for debugging
Filter logs by agent name using the thread prefix:
Retries
Tool failures can be automatically retried using the retry policy feature. This works alongside timeouts to handle transient errors:ToolConfig.parallel is a deprecated alias for ExecutionConfig.parallel_tool_calls. Enable parallel tool calls with execution=ExecutionConfig(parallel_tool_calls=True) alongside ToolConfig for timeout and retry. Do not set both spellings to conflicting values \u2014 that raises TypeError.Parallel tool calls inside one async turn
A singleastart(...) turn can itself dispatch multiple independent tool calls concurrently when parallel_tool_calls=True.
asyncio.gather(agent.astart(a), agent.astart(b)) runs two agents in parallel, while parallel_tool_calls=True runs several tools inside one agent turn in parallel.
Write-conflict guard for shell-like tools
Whenparallel_tool_calls=True batches two or more tool calls in one turn, PraisonAI runs them sequentially instead of concurrently if any pair could touch the same file. As of PraisonAI PR #4907, the guard also catches shell-like tools whose write target lives in a command string rather than a path/file_path argument — execute_command, acp_execute_command, and execute_code. Two calls to any of these in the same batch, or one of them alongside any other write, force sequential fallback. Path-only reads (read_file, list tools, etc.) still run concurrently.
Shared JSON state under concurrent writes
BaseJSONStore’s file lock now tolerates long critical sections without corruption. It judges a lock as stale by the lock file’s mtime age and refreshes that mtime with a background heartbeat while held, so a legitimately slow read-modify-write (large JSON write, slow disk) stays exclusive instead of being reclaimed by an impatient waiter and clobbered. See Thread Safety for details (PR #5136).
Related
Tool Retry Policy
Automatically retry failed tool calls with exponential backoff
Tool Configuration
Tool timeout settings and performance tuning
Async Bridge
Safe sync↔async boundary crossing utilities
Thread Safety
Chat history and state protection mechanisms

