Implementing Parallel Tool Execution with Claude: The Batch Meta-Tool Pattern
Claude supports parallel tool execution by default, but when the model serializes independent calls, wrapping multiple operations into a single batch meta-tool forces bundled execution and eliminates extra round-trip latency.
The anthropics/claude-cookbooks repository provides production-ready patterns for optimizing Claude integrations. In tool_use/parallel_tools.ipynb, the authors demonstrate how to overcome a subtle performance limitation: even with parallel tool use enabled in the API, Claude sometimes issues sequential tool calls that require multiple network round trips to resolve.
How Parallel Tool Execution Works in Claude
Claude's API includes specific parameters that control tool-calling behavior. Understanding these defaults is essential before implementing workarounds for serialization issues.
Default Parameter Values
When calling client.messages.create, the Anthropic API applies the following defaults unless overridden:
tool_choice: Defaults to{ "type": "auto" }, allowing Claude to select any available tool or combination of tools.disable_parallel_tool_use: Defaults toFalse, permitting the model to return multipletool_useblocks in a single response.
According to tool_use/parallel_tools.ipynb (lines 25-27), these defaults should theoretically allow Claude to request weather and time information simultaneously. However, the notebook demonstrates that model-level optimization often overrides this capability.
Why Claude Serializes Independent Calls
Even with disable_parallel_tool_use=False, Claude sometimes emits a single tool call followed by a second call only after receiving the first result. This occurs because the model optimizes for potential dependencies between operations. In tool_use/parallel_tools.ipynb (lines 14-15), an initial query requesting both weather and time returns only the get_weather call, forcing the client to respond before Claude requests get_time.
The Batch Meta-Tool Pattern
The recommended solution involves creating a batch meta-tool—a higher-order tool that internally dispatches multiple concrete operations. This pattern guarantees that Claude bundles related requests into a single tool_use block.
Define Concrete Tool Schemas
First, define the individual tools with proper JSON schemas:
def get_weather(location: str) -> str:
return f"The weather in {location} is 72 °F and sunny."
def get_time(location: str) -> str:
return f"The time in {location} is 12:32 PM."
weather_tool = {
"name": "get_weather",
"description": "Gets the weather for a given location",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
}
time_tool = {
"name": "get_time",
"description": "Gets the time for a given location",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
}
Create the Batch Wrapper Tool
Next, define a batch tool that accepts the common parameters and wraps both operations:
batch_tool = {
"name": "weather_and_time",
"description": "Returns weather and time for a location in parallel",
"input_schema": {
"type": "object",
"properties": {"location": {"type": "string"}},
"required": ["location"],
},
}
Register this alongside individual tools when creating the message:
from anthropic import Anthropic
client = Anthropic()
MODEL_NAME = "claude-sonnet-4-6"
def make_query(messages, tools):
return client.messages.create(
model=MODEL_NAME,
messages=messages,
max_tokens=500,
tool_choice={"type": "auto"},
tools=tools,
)
MESSAGES = [{"role": "user", "content": "What's the weather and time in San Francisco?"}]
response = make_query(MESSAGES, tools=[weather_tool, time_tool, batch_tool])
Handle Execution with Parallel Dispatch
Implement a handler that unpacks the batch call and executes sub-tools concurrently using asyncio.gather or thread pools:
import asyncio
async def process_batch_tool(tool_name, tool_input):
if tool_name == "weather_and_time":
loc = tool_input["location"]
# True parallel execution
weather, time = await asyncio.gather(
asyncio.to_thread(get_weather, loc),
asyncio.to_thread(get_time, loc)
)
return {"weather": weather, "time": time}
# Process Claude's response
for block in response.content:
if block.type == "tool_use":
result = await process_batch_tool(block.name, block.input)
print(f"Batch result: {result}")
As shown in tool_use/parallel_tools.ipynb (lines 201-207 and 287-293), Claude reliably selects the weather_and_time batch tool when available, reducing the interaction from two turns to one.
Visualizing Tool Interactions
The repository includes utilities for debugging parallel workflows. The tool_use/utils/visualize.py module (lines 135-144) provides render_tool_use() and render_tool_result() functions that format tool calls as tree structures:
from tool_use.utils.visualize import render_tool_use, render_tool_result
from rich.tree import Tree
tree = Tree("Claude Interaction")
for block in response.content:
if block.type == "tool_use":
render_tool_use(block, tree)
elif block.type == "tool_result":
render_tool_result(block, tree)
print(tree)
This visualization confirms whether Claude is bundling multiple operations into a single batch tool call or issuing separate sequential requests.
Summary
- Keep
disable_parallel_tool_useasFalse(the default) to allow the model to return multiple tool calls in principle. - Implement batch meta-tools when Claude serializes independent operations that could execute simultaneously.
- Register batch tools alongside individual tools to give Claude routing flexibility while ensuring it can choose the efficient bundled path.
- Use
asyncio.gatheror thread pools inside batch handlers to achieve true concurrent execution of sub-operations. - Leverage
tool_use/utils/visualize.pyto verify that batch tools are being utilized correctly.
Frequently Asked Questions
What is the default setting for parallel tool use in Claude?
By default, disable_parallel_tool_use is set to False and tool_choice defaults to {"type": "auto"}. These settings permit Claude to return multiple tool_use blocks in a single response, but do not guarantee that the model will choose to do so for independent operations.
Why does Claude sometimes call tools one at a time instead of in parallel?
Claude optimizes for potential dependencies between tool calls. If the model determines that the result of one tool might influence whether another tool is needed, or how it should be called, it will serialize the requests regardless of parallel settings. This is a model-level decision based on the perceived relationship between operations.
How do I force Claude to execute multiple tools simultaneously?
Expose a batch meta-tool that combines the inputs and outputs of multiple concrete tools into a single schema. When Claude recognizes that it needs all the information provided by the sub-tools, it will call the batch tool once, allowing your client code to execute the underlying operations in parallel using asyncio.gather or similar concurrency primitives.
Can I use async/await with Claude's tool execution?
Yes. While the Anthropic Python client supports both sync and async APIs, the parallel execution happens in your tool implementation. As shown in tool_use/parallel_tools.ipynb, you should use asyncio.gather() or asyncio.to_thread() inside your batch tool handler to run multiple sub-tools concurrently, then return the aggregated results to Claude in a single tool_result block.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →