Sequential vs Parallel Tool Execution in Agents
When should an agent run tools in parallel and when must it wait? Getting parallel vs sequential tool calls wrong either wastes time or corrupts results. Here's how to decide, with code.
def execute_tools(calls):
groups = topological_groups(calls) # order by dependency
results = {}
for group in groups: # each group is independent within
batch = parallel_map(
lambda c: (c.id, tools[c.name](**resolve(c.args, results))),
group,
)
results.update(batch) # feeds the next group
return resultsWhen should your agent run tools at the same time, and when must it run them one after another? Get parallel vs sequential tool calls wrong in one direction and you leave speed on the table, running independent operations in a slow series. Get it wrong in the other direction and you corrupt results, running dependent operations concurrently so one reads data the other hasn't written yet. The decision isn't stylistic — it's about which operations actually depend on each other, and this guide gives you a concrete way to decide.
The rule underneath it all is simple: operations that don't depend on each other can run in parallel; operations where one needs another's result must run in sequence. The skill is detecting which is which reliably, because a wrong guess in either direction has a real cost.
Quick-Start (Copy This Right Now)
[object Object], ,[object Object],(,[object Object],):
groups = topological_groups(calls) ,[object Object],
results = {}
,[object Object], group ,[object Object], groups: ,[object Object],
batch = parallel_map(
,[object Object], c: (c.,[object Object],, tools[c.name](**resolve(c.args, results))),
group,
)
results.update(batch) ,[object Object],
,[object Object], resultsWhat this does: sorts tool calls into dependency-ordered groups, runs each group's independent calls in parallel, and passes results forward so a dependent call in a later group sees what it needs — parallel where safe, sequential where required, automatically.
This is the whole pattern: group by dependency, parallelize within a group, sequence across groups. Everything else is how you detect the dependencies.
It's worth noticing that this reduces to a problem software has solved for decades — a topological sort over a dependency graph. Agents make it feel novel because the "graph" is proposed on the fly by a model rather than written by a programmer, but the execution logic underneath is the same well-understood machinery. That's reassuring: you're not inventing a concurrency model, you're applying a standard one to a set of nodes the model hands you each turn.
Understanding the Variables
Three things determine whether two tool calls can run together.
Data dependency — does call B use call A's output as input? If yes, B must wait. Side-effect dependency — do they write to the same resource, where order changes the outcome? Two writes to the same record can't safely race. And independence — no shared input, no shared mutable resource — which means they're free to run concurrently. Most read-only lookups across different entities are independent; most writes to shared state are not.
The default should be sequential only where you can show a dependency, and parallel everywhere else — because independence is common and waiting is expensive. But that default flips for writes: there, assume you must sequence unless you can prove the writes don't conflict.
The asymmetry between reads and writes is the crux, and it's worth internalizing as a rule of thumb. A read has no consequences the next call can trip over — two reads of different data can happen in any order or at the same time and the world is unchanged. A write changes shared reality, so two writes racing can interleave into a state neither intended. That's why the safe defaults point in opposite directions: reads are innocent until a dependency is proven, writes are guilty until independence is proven. Most parallelism bugs come from applying the read default to a write.
⚡ Pro tip: Reads parallelize freely; writes rarely do. A quick heuristic that's right most of the time: fan out read operations concurrently, and serialize anything that mutates shared state unless you've explicitly confirmed the mutations touch different resources.
Step-by-Step: Deciding Parallel vs Sequential Tool Calls
Work through three questions for any set of calls.
First, does any call consume another's output? Trace the arguments — if call B's input references call A's result, that's a data dependency and B goes in a later group. Second, do any calls write to the same place? If two calls mutate the same record, file, or counter, order matters and they must be sequenced. Third, everything left over is independent and parallelizes.
[object Object], ,[object Object],(,[object Object],):
groups, placed = [], ,[object Object],()
,[object Object], ,[object Object],(placed) < ,[object Object],(calls):
ready = [c ,[object Object], c ,[object Object], calls
,[object Object], c.,[object Object], ,[object Object], ,[object Object], placed
,[object Object], deps(c).issubset(placed)] ,[object Object],
,[object Object], ,[object Object], ready:
,[object Object], ValueError(,[object Object],)
groups.append(ready) ,[object Object],
placed.update(c.,[object Object], ,[object Object], c ,[object Object], ready)
,[object Object], groupsWhat this does: repeatedly gathers the calls whose dependencies are already satisfied into a parallel-safe group, then advances — producing an execution plan that's maximally parallel while never running a call before its inputs exist, and catching dependency cycles instead of deadlocking.
⚠️ Common mistake: Parallelizing calls that share a mutable resource because they look independent. Two calls that both update the same inventory count have no obvious data dependency — neither uses the other's output — but running them concurrently races, and one update silently overwrites the other. Independence means no shared output target either, not just no shared input. Check what each call writes, not only what it reads.
Pro-Level Variations
The right concurrency posture shifts by workload.
A market-data engineer's agent parallelizes aggressively — nearly everything is a read from a different symbol, so fanning out dozens of independent price lookups is pure speedup with no risk. A fintech payments agent sequences almost everything — most operations mutate balances, and a race means real money lost, so it serializes by default and parallelizes only confirmed-independent reads. A content-pipeline agent runs a mix: parallel fetches to gather sources, then a strict sequence for the write-and-publish steps that depend on each other.
Same decision framework, three different default postures — set by whether the task is read-heavy or write-heavy.
⚡ Pro tip: Make the model declare each call's dependencies explicitly rather than inferring them. Ask it to tag every tool call with what it depends on ("depends_on: []" or "depends_on: [call_2]"). An explicit declaration you can validate beats a guess your executor reverse-engineers from argument matching, which misses dependencies that don't show up as shared arguments.
⚡ Pro tip: Set your default by workload. Read-heavy agents default to parallel and mark the rare dependency; write-heavy agents default to sequential and mark the rare safe parallel. Choosing the right default for the task means you're annotating exceptions, not every call.
Troubleshooting Common Issues
If your agent produces inconsistent results that change between runs, you're probably parallelizing calls that share mutable state — a race condition. Serialize anything that writes to a shared resource and see if the flakiness vanishes.
If your agent is slower than it should be, you're over-sequencing — running independent reads one at a time. Check whether the calls you're sequencing actually depend on each other; often they don't and you've serialized out of caution.
If your loop deadlocks or errors on dependencies, you likely have a cycle — call A needs B and B needs A. That's a planning bug in how the model proposed the calls; catch it explicitly rather than letting the executor hang forever.
⚡ Pro tip: When unsure whether two calls are safe to parallelize, run them sequentially. A sequential run that's a little slower is always correct; a parallel run that races is sometimes wrong in ways that are miserable to debug. Correctness first, then optimize the cases you've proven safe.
Your Turn
Take an agent that runs tools one at a time and add dependency grouping: trace which calls consume others' outputs, flag which write to shared state, and parallelize the rest. Start conservative — parallelize only obvious independent reads — and widen as you confirm what's safe. Even parallelizing just the read-only lookups usually delivers most of the speedup.
That's the pragmatic path in a sentence: don't try to build a perfect dependency analyzer on day one. Parallelize the calls you can see are independent reads, leave everything else sequential, and you'll capture the large majority of the available speedup with none of the race-condition risk. Widen the parallel set only as you prove specific patterns safe. Correctness is the floor you never trade away; speed is the thing you buy back incrementally once you're sure it's free.
One last framing to carry with you: parallelism in an agent isn't a performance feature you sprinkle on at the end — it's a correctness decision you make per operation. Every call either can or can't safely run alongside its neighbors, and that answer is a property of the operation, not a knob you tune for speed. Treat it that way and the fast version and the correct version are the same version.
The dependency-grouping executor is reusable across every agent that runs multiple tools. I keep it saved and versioned in PromptABCD, so a new agent gets correct parallel vs sequential tool calls handling by default — fast where it's safe, ordered where it must be — instead of a naive executor that's either needlessly slow or occasionally, mysteriously wrong.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
