I run tool calls together only when they have permission, valid inputs and no dependencies or shared-state conflicts. If one call needs another’s result, I keep them in order.
For example, I can check an order and fetch delivery guidance together if both already have their inputs. A refund needs its own authorisation and sequence.
My approach follows 3 steps:
- Select: Check the agent’s state, permissions, arguments and dependencies. Record which calls can start.
- Run: Limit concurrent calls, queue the rest and set timeouts. Before retrying, check whether the action finished; reuse its idempotency key when a retry is safe.
- Merge: Match results to call IDs, keep successful results, flag gaps or conflicts and update the state once. Track queued and incomplete calls until completion or deadline.
I test failure paths before enabling autonomous execution. That includes rejected calls, queue limits, cancellation and uncertain outcomes that need human review. I then compare latency, reliability and £ per completed task with serial execution, using the same workload and keeping sensitive data out of logs.
::: @figure
{Conditional Parallel Tool Execution: Select, Run, Merge}
:::
#11 · Parallel Tool Calls – Make Your AI Agent Faster | Agentic AI Course
Step 1: Check Conditions and Select Tool Calls
Use a router to turn the current agent state into a call plan: no calls, one call, or a parallel batch. Apply concurrency limits in the next step.
Check Permissions, Arguments and Dependencies
Confirm that tool use is needed, the caller has permission to use the tool, and the arguments match the schema and current prerequisites. Defer the call if inputs are missing.
Treat user-controlled content as untrusted. Keep agents that read it separate from agents with access to private data or external communications [1].
Check data dependencies before scheduling calls in parallel.
Record the Call Plan and Dependencies
Record the routing decision and dependencies for each planned call. Only independent, eligible calls should run in parallel.
plan = router.select(current_state) # may contain zero calls save(plan.ids, plan.dependencies, status="selected") # Record the selected plan if plan.is_empty(): continue_reasoning_without_tools() else: ready = evaluate_conditions_and_dependencies(plan, current_state) independent, serial = separate_independent_calls(ready) batch = independent if independent else serial.take_one() # Queue only the ready batch mark_queued(batch.ids)
The next step executes only the ready batch, within concurrency limits.
Step 2: Run Calls Within Concurrency Limits
Take the ready batch from Step 1 and run only eligible, independent calls through a bounded worker pool. Queue any excess calls. Under each call ID, record its status, result and error. Then standardise the returned results before reasoning continues.
Set Limits, Timeouts and Retry Rules
Size the semaphore or worker pool to match the available connections and worker slots. Apply rate limits where needed to stay within the downstream service’s request capacity.
Set a timeout for each call and cap retries, using backoff for temporary failures. Before retrying a call that times out or receives no response, check its external status.
If completion is confirmed, record the receipt and do not repeat the action. If completion is not confirmed, retry only when it is safe, using the same idempotency key. If the call is still processing, wait before checking again. Send any outcome that remains unknown for human review.
Compare Serial and Parallel Execution
| Execution model | Latency | Resource use | Cost impact | Risk |
|---|---|---|---|---|
| Serial | High (sequential) | Low (single executor) | Predictable/low | Low (no contention) |
| Bounded parallelism | Medium/low | Moderate (limited pool) | Balanced | Managed (semaphore/pool) |
Run the same call plan under serial and bounded execution, measuring total elapsed time, resource use and cost impact. Bounded parallelism lowers latency while keeping calls within downstream limits. Serial execution is simpler, but slower.
Step 3: Combine Results and Continue Reasoning
After execution, merge the outcomes against the recorded call plan, including queued and incomplete calls. Track every selected call until it reaches a terminal status or its deadline expires - even queued calls that never started.
Standardise Results and Keep Partial Success
Keep usable results from successful calls, and clearly label any missing data.
Resolve Conflicts and Update Agent State
Use fixed rules to handle duplicate fields and conflicting values. Flag anything those rules cannot resolve.
Validate the merged results, then apply a single state update. Continue only when dependencies are satisfied. Otherwise, request the missing information and leave the item unresolved. Re-check the updated state before the next reasoning turn.
Conclusion: Test and Run the Pattern Safely
Test Routing, Queues and Partial Failures
Test four cases: no eligible calls, one eligible call, several independent calls and mixed dependencies. Check which calls may start together, not just which results appear. Include permission denials and rejected arguments.
Also test over-limit batches, timeouts, cancellation, retries and partial failures. Verify idempotency, confirm that successful results are preserved and check that each result matches its call ID.
Check Readiness for Production
Once routing and failure-path tests pass, use a release checklist to check readiness for rollout. Cover condition logic, dependency checks, run-time authorisation, concurrency and rate limits, result correlation and timeout handling. Every check must pass before you enable autonomous execution.
Monitor routing decisions, queue depth and outcomes, but leave out sensitive data. Compare latency, reliability and cost (£ per completed task) against serial execution, using the same workload mix for both runs.
FAQs
How can I spot hidden dependencies between tool calls?
Map failure paths and business dependencies before you start coding [1]. Look for shared state and competition for resources, such as database connections or file handles. Tests that pass on their own but fail when run in parallel often point to order dependencies, race conditions or resource conflicts [2].
Keep each call self-contained and isolated. Check whether globals, singletons or caches are being changed at the same time. Use identifiers such as UUIDs or timestamps to keep data separate [2].
How do I choose the right concurrency limit?
Balance performance targets with your infrastructure’s capacity and cost. Start with your hardware: a common rule of thumb is to match the parallelism limit to the available CPU cores to reduce resource contention. Track resource usage and latency to spot bottlenecks before they affect stability.
Use circuit breakers to switch off parallel stages automatically when latency or GPU costs exceed set thresholds. This helps keep the system reliable and spending within budget.
When are partial results safe to use?
Partial results are safe only when the system handles failures and keeps state strictly consistent. Handlers must be idempotent: processing the same input again must produce the same outcome without duplicate side effects.
For multi-step tasks, save durable checkpoints and the context behind each decision. This lets processing resume from a known state instead of blindly replaying transactions. Quarantine uncertain results, and make sure compensating actions can reverse or correct unintended state changes before processing continues.