Claude API Parallel Tool Calls: Implementation Guide
What "parallel tool calls" actually means in the Claude API
When you give Claude more than one tool and a request that needs several independent lookups — say, weather for three cities, or a database query plus a price check — Claude can return multiple tool_use blocks in a single assistant turn instead of calling them one at a time. This is what people mean by "parallel tool calls": not that Claude executes the tools itself, but that it asks for several tool invocations at once, and your code is responsible for running them (ideally concurrently) and sending back all the results together.
The implementation challenge isn't the Claude API call itself — it's the orchestration on your side: detecting multiple tool_use blocks, executing them concurrently, matching each result back to the correct tool_use_id, handling partial failures, and sending a single user message back with all tool_result blocks in the right order. This article walks through that implementation end to end.
How Claude signals a parallel tool call request
When Claude decides to call tools, the response content array can contain more than one tool_use object alongside any text blocks. A typical response looks like this:
{
"role": "assistant",
"content": [
{ "type": "text", "text": "Let me check both." },
{ "type": "tool_use", "id": "toolu_01A", "name": "get_weather", "input": { "city": "Lisbon" } },
{ "type": "tool_use", "id": "toolu_01B", "name": "get_weather", "input": { "city": "Porto" } }
],
"stop_reason": "tool_use"
}
Two things matter here: stop_reason is "tool_use" (not "end_turn"), and there are two separate id values. Your job is to execute both, then return two tool_result blocks referencing those exact IDs.
Step 1: detect and collect all tool_use blocks
Don't assume there's only one. Filter the content array for every block with type === "tool_use":
const toolUseBlocks = response.content.filter(
(block) => block.type === "tool_use"
);
If toolUseBlocks.length is zero, Claude finished normally. If it's one or more, you need to execute all of them before continuing the conversation.
Step 2: execute them concurrently
This is the part most implementations get wrong by looping with await sequentially. Use Promise.all (or Promise.allSettled if you want to handle failures gracefully) so independent tool calls actually run in parallel:
const toolHandlers = {
get_weather: async (input) => fetchWeather(input.city),
get_price: async (input) => fetchPrice(input.sku),
};
const results = await Promise.allSettled(
toolUseBlocks.map(async (block) => {
const handler = toolHandlers[block.name];
if (!handler) throw new Error(`Unknown tool: ${block.name}`);
const output = await handler(block.input);
return { id: block.id, output };
})
);
Promise.allSettled is the safer choice because one failing tool call shouldn't crash the rest. Each tool should fail independently and report its own error back to Claude.
Step 3: build matching tool_result blocks
Each tool_result must carry the exact tool_use_id it answers. Order within the array doesn't strictly matter to Claude, but mismatched IDs will break the turn:
const toolResults = results.map((result, i) => {
const toolId = toolUseBlocks[i].id;
if (result.status === "fulfilled") {
return {
type: "tool_result",
tool_use_id: toolId,
content: JSON.stringify(result.value.output),
};
}
return {
type: "tool_result",
tool_use_id: toolId,
content: result.reason.message,
is_error: true,
};
});
Then send a new user message containing all toolResults as its content, appended to the conversation history, and call the API again. Claude will read every result and continue the turn — often producing a final text answer, but sometimes requesting another round of parallel tool calls if the first results revealed more work to do. Loop this pattern until stop_reason is "end_turn".
Common implementation mistakes
- Awaiting tools one by one. If three tools each take 2 seconds, sequential execution costs 6 seconds; concurrent execution costs roughly 2.
- Dropping failed calls instead of reporting them. Claude needs a
tool_resultfor everytool_use_id, even if the tool errored. Omitting one leaves the turn incomplete and the next request will fail validation. - Returning results out of order relative to a different
tool_use_idmapping. Always index by ID, never assume array position is preserved afterPromise.allSettled. - Not capping concurrency. If a request triggers ten tool calls that each hit a rate-limited third-party API, blast all ten at once and you'll get throttled. Use a small concurrency limiter (e.g., batches of 4–5) for high-fanout cases.
- Forgetting timeouts. Wrap each tool handler in a timeout so one slow external API doesn't stall the whole batch indefinitely.
Simplifying this with SubToAPI
If you're building this on top of your own Claude access rather than a raw Anthropic account, SubToAPI turns that access into a standard HTTPS endpoint with application API keys, so the parallel tool call logic above is the only orchestration layer you need to write — no separate auth or key rotation concerns. The request and response shapes for tool use match what's described in the tool use docs and the messages docs, so the Promise.allSettled pattern works unchanged against sub_live_... keys. Streaming responses that include tool calls are covered in the streaming docs, and a minimal end-to-end setup is in the quickstart. Plans start at the pricing page, with a free trial at signup.
Questions
Does Claude execute tools in parallel on Anthropic's servers? No. Claude only decides which tools to call and returns multiple tool_use blocks in one response. Your application executes the actual tool functions, and you choose whether to run them concurrently.
What happens if one tool call fails while others succeed? Return a tool_result for every tool_use_id, marking the failed one with is_error: true and a short message. Claude will see which calls succeeded and which failed, and can decide whether to retry, ask for clarification, or proceed with partial data.
Can Claude request another round of parallel tool calls after receiving results? Yes. After you send back tool_result blocks, Claude may respond with stop_reason: "tool_use" again if it needs more information. Keep looping the execute-and-respond cycle until you get stop_reason: "end_turn".