
Fetch Multiple Data Points in One Claude Response: Parallel Tool Calls With asyncio.gather
Chris Harper
3 min read
Aug 10, 2026 · 20:03 UTC
Claude 4 and later return multiple independent tool calls in a single response — dispatch them with asyncio.gather to cut latency by N× instead of running sequential round trips.
What you'll be able to do after this:
- Understand how Claude signals parallel tool intent in a single response
- Dispatch multiple tool calls concurrently with
asyncio.gatherand return all results in one user message - Know when to disable parallel tool use for sequential, side-effectful workflows
The serial trap
Most agents are written as a simple while loop: call the API, get a tool request, execute it, return the result, repeat. If your agent needs to fetch a user profile, look up their orders, AND check their billing status, that is three sequential round trips — the total latency is T₁ + T₂ + T₃. Claude doesn't need them to be sequential; your code forces it.
How Claude signals parallel intent
Claude 4 and later models return multiple tool_use blocks in a single assistant message when the calls are independent:
# Claude returns ONE assistant message containing N tool_use blocks
response.content = [
ToolUseBlock(id="toolu_01", name="get_user_profile", input={"user_id": "u123"}),
ToolUseBlock(id="toolu_02", name="get_orders", input={"user_id": "u123"}),
ToolUseBlock(id="toolu_03", name="get_billing", input={"user_id": "u123"}),
]
Your responsibility: run all three, then return ALL results in a single user message before calling the API again.
The parallel dispatch pattern
import asyncio
import anthropic
client = anthropic.Anthropic()
async def dispatch_parallel(tool_calls, tool_map):
"""Run all tool calls concurrently; return tool_result blocks."""
tasks = [tool_map[tc.name](**tc.input) for tc in tool_calls]
results = await asyncio.gather(*tasks, return_exceptions=True)
return [
{
"type": "tool_result",
"tool_use_id": tc.id, # match by id, not by position
"content": str(r) if not isinstance(r, Exception) else f"Error: {r}",
"is_error": isinstance(r, Exception),
}
for tc, r in zip(tool_calls, results)
]
async def agent_loop(prompt, tools, tool_map):
messages = [{"role": "user", "content": prompt}]
while True:
resp = client.messages.create(
model="claude-sonnet-5-20250901",
max_tokens=1024,
tools=tools,
messages=messages,
)
if resp.stop_reason == "end_turn":
return next(b.text for b in resp.content if hasattr(b, "text"))
tool_calls = [b for b in resp.content if b.type == "tool_use"]
# 1. Append the full assistant message (keeps tool_use blocks intact)
messages.append({"role": "assistant", "content": resp.content})
# 2. Dispatch all calls in parallel
results = await dispatch_parallel(tool_calls, tool_map)
# 3. Return ALL results in a single user message
messages.append({"role": "user", "content": results})
Two rules that catch most bugs
Match by tool_use_id, not list position. Claude doesn't guarantee which tool appears first. Always pair calls and results by id.
Append response.content in full — never strip tool_use blocks. The next API call expects to see Claude's own requests echoed back. Stripping them causes an API error.
Disable for sequential workflows
If your tools have side effects or ordering dependencies (e.g., create_invoice must succeed before send_confirmation):
client.messages.create(
...,
tool_choice={"type": "auto", "disable_parallel_tool_use": True},
)
Sources: Parallel tool use — Claude Platform Docs · Implement tool use — Claude Platform Docs · Build a tool-using agent — Claude Platform Docs