Claude API Tool Use: Handling Parallel Calls
When Claude decides it needs more than one tool to answer a request, it doesn't always ask for them one at a time. In a single turn it can return multiple tool_use blocks in the same response — for example, calling a weather API and a calendar lookup together instead of sequentially. This is "parallel tool calls," and handling it correctly is the difference between an agent that feels fast and one that silently breaks on multi-step requests.
This article covers how parallel tool calls show up in the Claude API response format, how to execute them concurrently in your code, and how to send the results back without corrupting the conversation state.
How Claude signals parallel tool calls
When Claude wants to use tools, it returns a message with stop_reason: "tool_use" and a content array. If Claude needs multiple tools at once, that array contains more than one tool_use block, each with its own id, name, and input:
{
"stop_reason": "tool_use",
"content": [
{ "type": "text", "text": "I'll check both the weather and your calendar." },
{ "type": "tool_use", "id": "toolu_01A", "name": "get_weather", "input": { "city": "Lisbon" } },
{ "type": "tool_use", "id": "toolu_01B", "name": "get_calendar", "input": { "date": "2025-04-02" } }
]
}
Nothing in the request forces this behavior — it's a model decision based on whether the tasks are independent. You can't "turn on" parallel calls with a flag, but you can make them more likely by:
- Giving each tool a clear, narrow description so Claude doesn't need to chain them sequentially
- Mentioning in the system prompt that independent lookups can be done together
- Avoiding tool designs where one tool's output is obviously required as input to another (that forces sequential calls, correctly)
Executing multiple tool calls concurrently
The common mistake is looping over tool_use blocks and calling each tool with await in sequence, which defeats the purpose. Use Promise.all (or your language's equivalent) so independent tool calls run concurrently:
const toolUses = response.content.filter(block => block.type === "tool_use");
const results = await Promise.all(
toolUses.map(async (block) => {
const output = await runTool(block.name, block.input);
return {
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(output),
};
})
);
runTool is your own dispatcher — a switch statement or lookup table mapping tool names to functions. If one tool call fails, catch the error inside the map function and return an error string as the content, rather than letting Promise.all reject and losing the other results:
async function runToolSafely(name, input) {
try {
const output = await runTool(name, input);
return JSON.stringify(output);
} catch (err) {
return JSON.stringify({ error: err.message });
}
}
Claude handles tool-level errors gracefully as long as you return something in the content field — it will often retry with different input or explain the failure to the user instead of crashing your app.
Returning all results in one user message
This is the part people get wrong most often: all tool_result blocks from a parallel call must go back in a single user message, matching each tool_use_id. Sending them as separate messages, or omitting one, will cause the API to reject the request or produce an incoherent reply.
{
"role": "user",
"content": [
{ "type": "tool_result", "tool_use_id": "toolu_01A", "content": "{\"temp_c\": 18, \"condition\": \"cloudy\"}" },
{ "type": "tool_result", "tool_use_id": "toolu_01B", "content": "{\"events\": [\"Dentist 10am\"]}" }
]
}
Order doesn't need to match the original tool_use order, but every tool_use_id from the previous turn must be accounted for. If you skip one because a tool call wasn't applicable, you still need to return a tool_result for it — use an empty or explanatory string rather than leaving it out.
Putting it together in a loop
A minimal agent loop that handles both single and parallel tool calls looks like this:
let messages = [{ role: "user", content: userInput }];
while (true) {
const response = await client.messages.create({
model: "claude-sonnet-4",
max_tokens: 1024,
tools,
messages,
});
messages.push({ role: "assistant", content: response.content });
if (response.stop_reason !== "tool_use") {
return response; // final answer, no more tools needed
}
const toolUses = response.content.filter(b => b.type === "tool_use");
const toolResults = await Promise.all(
toolUses.map(async (block) => ({
type: "tool_result",
tool_use_id: block.id,
content: await runToolSafely(block.name, block.input),
}))
);
messages.push({ role: "user", content: toolResults });
}
This loop works whether Claude requests one tool or five in a given turn — the Promise.all and the batched tool_result message handle both cases without special branching.
Latency and reliability considerations
Parallel tool calls are mainly a latency optimization — three independent API lookups running concurrently instead of sequentially can cut response time significantly. But concurrency introduces its own failure modes: rate limits on downstream APIs, partial failures, and timeout mismatches between tools (a 200ms lookup and a 5s report generator in the same batch).
Set a per-tool timeout and resolve it as an error result rather than letting one slow tool hold up the entire batch:
function withTimeout(promise, ms) {
return Promise.race([
promise,
new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms)),
]);
}
If you're running tool-using agents through SubToAPI, the same patterns apply directly — SubToAPI proxies the standard Claude messages format, so your tool loop and parallel-call handling code don't need to change. The dashboard's usage metadata also makes it easier to spot when a specific tool call is adding latency or token cost across a team, which is useful when you're debugging a batch of parallel calls that's running slower than expected. See the tool use docs and the messages API reference for the exact request shape.
Frequently asked questions
Does Claude always call tools in parallel when it could? No. Parallel tool use is a model decision based on whether the tasks look independent. Clear, narrowly scoped tool descriptions make it more likely, but there's no API parameter that forces concurrent calls.
What happens if I don't return a tool_result for every tool_use block? The next API call will typically fail or Claude will respond inconsistently, since it's waiting on results for every tool_use_id it issued. Always return a tool_result for each one, even if it's just an error message.
Can I limit how many tools Claude calls in parallel? Not directly through the API. If you need to cap concurrency (e.g., a rate-limited downstream service), enforce it in your own tool dispatcher — process the tool_use blocks with a concurrency limit instead of raw Promise.all when the batch is large.