When your LLM agent needs to invoke multiple functions, the decision between parallel vs sequential tool calls latency profiles determines whether users wait seconds or minutes. Parallel execution packs multiple independent invocations into a single model round-trip, while sequential chaining forces a separate inference call per step. Below we compare both patterns across capability, cost, and orchestration complexity using real OpenAI-compatible schemas.
How the two patterns work
The OpenAI chat completions API represents a model’s intent to call a function through message.tool_calls, an array. In sequential usage that array has length one; in parallel it can hold many. The wire format is identical—only your client loop differs.
Sequential tool calls
The model returns one tool call, you execute it, feed the result back as a role: "tool" message, and loop until it responds with plain text. This is the original ReAct-style agent loop and is supported by every model that implements function calling.
from openai import OpenAI
client = OpenAI()
messages = [{"role": "user", "content": "Get weather and stock price for AAPL"}]
tools = [
{"type": "function", "function": {"name": "get_weather", "parameters": {...}}},
{"type": "function", "function": {"name": "get_quote", "parameters": {...}}},
]
while True:
resp = client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools)
msg = resp.choices[0].message
if not msg.tool_calls:
print(msg.content)
break
tc = msg.tool_calls[0] # exactly one in sequential
result = dispatch(tc)
messages.append({"role": "tool", "tool_call_id": tc.id, "content": result})
Parallel tool calls
Modern models can emit several tool_calls in one message. You run them concurrently with any async runtime.
import asyncio
resp = client.chat.completions.create(model="gpt-4o", messages=messages, tools=tools)
msg = resp.choices[0].message
if msg.tool_calls:
async def run_all():
return await asyncio.gather(*[dispatch_async(tc) for tc in msg.tool_calls])
results = asyncio.run(run_all())
for tc, res in zip(msg.tool_calls, results):
messages.append({"role": "tool", "tool_call_id": tc.id, "content": res})
The key difference in parallel vs sequential tool calls latency is the number of model round-trips: sequential multiplies them by the step count, parallel collapses independent steps into one.
Latency and throughput
Assume a model inference hop of ~800 ms and each tool backend takes 200 ms. For five independent tools:
- Sequential: 5 × (800 ms + 200 ms) ≈ 5.0 s wall clock.
- Parallel: 800 ms + max(200 ms) ≈ 1.0 s wall clock.
The gap in parallel vs sequential tool calls latency grows linearly with step count. Throughput on the model side is similar in token terms, but sequential consumes more total request slots because of repeated calls. If your provider rate-limits requests per minute, sequential burns them faster.
One caveat: the model must generate a larger output payload for parallel calls, adding maybe 50–100 ms of generation time per extra call. That still beats an extra 800 ms round-trip. Streaming does not help here—tool calls arrive in the final message, not as tokens you can act on early.
Cost model
LLM billing is per input + output token. Sequential re-sends the entire conversation history on every turn, so the input tokens grow linearly with steps. Parallel sends the prompt once and gets one response.
A gateway such as n4n.ai exposes per-token usage metering and honors client routing directives, making the token delta between these strategies observable without custom instrumentation. In our traces, a 5-step sequential chain cost ~3× the input tokens of the parallel equivalent for the same task.
Output tokens are usually lower per step for sequential because each step emits fewer tokens, but the repeated input dominates. If tool results are massive (e.g., full document text), parallel forces you to inject all of them at once, possibly blowing the context window; sequential can summarize between steps.
Capabilities and dependencies
Parallel only works when tool calls are independent. If step 2 needs the JSON from step 1, you cannot fork. Sequential handles arbitrary dependency graphs by construction.
Models sometimes mistakenly emit parallel calls that actually depend on each other (e.g., “get user ID” then “get orders for user ID” in the same array). You must detect this and fall back to sequential, or you will get runtime errors. A simple heuristic: if any function’s parameters reference a value not in the prompt, force a sequential re-plan.
Ergonomics and orchestration
Sequential is a simple while loop. Parallel demands:
- Concurrent dispatch with timeout/cancellation.
- Merging results into the message list preserving
tool_call_id. - Handling partial failure: if one of five calls fails, do you retry all or just that one?
async def dispatch_with_timeout(tc, sec=5):
try:
return await asyncio.wait_for(run_tool(tc), sec)
except Exception as e:
return f"error: {e}"
# later
results = await asyncio.gather(*[dispatch_with_timeout(tc) for tc in msg.tool_calls])
Sequential sidesteps these with linear error handling: a try/except around dispatch and a user-visible message. Testing parallel agents requires mocking concurrent paths; sequential is easier to step through in a debugger.
Ecosystem support
OpenAI’s chat completions API has supported multiple tool_calls since GPT-4 Turbo. Anthropic’s Claude messages API returns an array of tool use blocks. Open-source models vary; many fine-tunes emit only single calls. An OpenRouter-class endpoint that addresses 240+ models will forward whatever the underlying provider returns, so parallel support is per-model, not per-gateway.
Limits: APIs cap the number of tool calls per response. In practice, stable parallel batches are under a dozen; larger fan-outs need manual map-reduce across turns. Context windows also bound how many results you can stuff back. Provider rate limits on requests-per-minute hit sequential harder; token-per-minute limits hit parallel harder when results are large.
Comparison table
| Dimension | Parallel tool calls | Sequential tool calls |
|---|---|---|
| Round-trips to LLM | 1 for N independent calls | N (one per call) |
| Wall-clock latency | Lowest (overlap backend I/O) | Linear in steps |
| Token cost (input) | Single context send | Multiplied by steps |
| Dependency handling | Independent calls only | Arbitrary chains |
| Orchestration code | Async gather, partial failure | Simple loop |
| Ecosystem maturity | Newer, model-dependent | Universal |
| Failure blast radius | Multiple calls at risk | Isolated per step |
| Context pressure | All results at once | Incremental |
Limits and failure modes
Parallel fan-out amplifies rate limits on your own tool backends. If you call 10 internal APIs at once, you may exceed their concurrency caps. Sequential naturally throttles.
Model quality degrades with too many parallel calls; it may hallucinate arguments when packing many functions. Keep the tool list narrow per request. Partial parallel failure forces a decision: retry the whole batch (wasteful) or patch the missing result and continue (risks inconsistent state). Sequential fails one step at a time and can recover with a clarification turn.
Which to choose
Independent read-only fetches (lookup, weather, DB reads): Use parallel. The parallel vs sequential tool calls latency win is decisive and the code overhead is justified.
Dependent chains (book then pay, query then filter): Sequential is mandatory. Attempting parallel will produce invalid args or runtime errors.
Mixed workflows: Start parallel for the independent front-end fan-out, collect results, then enter a sequential refinement loop if the model needs to reason over them. This hybrid is the most common production pattern.
High-rate-limit backend: Sequential or bounded concurrency (e.g., asyncio.Semaphore(3)) to avoid self-DoS. Parallel is not free of orchestration cost.
Cost-sensitive, long context: Sequential may be cheaper if the tool results are huge and you would otherwise duplicate massive context; but usually parallel wins on input tokens.
Prototyping: Default to sequential. It is easier to log, debug, and explain. Switch to parallel only after profiling shows round-trip latency is the bottleneck.
Pick based on dependency graph, not on fashion. Measure with per-token metering before optimizing, and keep the tool list tight regardless of pattern.