C.W.K.
Stream
Lesson 02 of 06 · published

Parallel Tool Calls in One Round

~14 min · parallel, tool-use, concurrency

Level 0Observer
0 XP0/64 lessons0/13 achievements
0/150 XP to next level150 XP to go0% complete

The model can call multiple tools per turn

When the user asks a compound question ('weather in Seoul and Tokyo'), Claude returns multiple tool_use blocks in a single assistant turn. You execute them, collect the results, and send all tool_result blocks back in one user turn. Round-trips drop from N+1 to 2.

Concurrency is your job

The protocol delivers parallel tool_use blocks; running them concurrently is your code's job. asyncio.gather or Promise.all over the blocks is the obvious move. Make sure tool order in the response matches the tool_use_ids — the model uses ids to associate results.

When to disable parallel

If your tools have ordering dependencies (call A before B), pass tool_choice: {"type": "tool", "name": "A"} on the first round and a different choice on the second. Or set disable_parallel_tool_use: True in tool_choice to force sequential. Use sparingly — most tools are independent.

Principle: Parallel tool use is a free latency win when tools are independent. Treat parallel as the default; force sequential only when the dependency is real.

Code

Run all tool_use blocks concurrently·python
import asyncio, json
from anthropic import AsyncAnthropic

client = AsyncAnthropic()

async def execute(block):
    handler = HANDLERS[block.name]
    result = await handler(**block.input)
    return {
        "type": "tool_result",
        "tool_use_id": block.id,
        "content": json.dumps(result),
    }

async def round_with_parallel(messages):
    resp = await client.messages.create(
        model="claude-sonnet-4-6",
        max_tokens=1024,
        tools=TOOLS,
        messages=messages,
    )
    messages.append({"role": "assistant", "content": resp.content})
    if resp.stop_reason != "tool_use":
        return resp, messages
    tool_blocks = [b for b in resp.content if b.type == "tool_use"]
    results = await asyncio.gather(*(execute(b) for b in tool_blocks))
    messages.append({"role": "user", "content": results})
    return resp, messages
Force a specific tool or disable parallel·python
# Force the model to call THIS tool first.
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=TOOLS,
    tool_choice={"type": "tool", "name": "validate_input"},
    messages=messages,
)

# Or disable parallel calls when downstream operations are not independent.
resp = client.messages.create(
    model="claude-sonnet-4-6",
    max_tokens=1024,
    tools=TOOLS,
    tool_choice={"type": "auto", "disable_parallel_tool_use": True},
    messages=messages,
)

External links

Exercise

Modify your tool loop so any single round runs all tool_use blocks concurrently. Run a benchmark with 3 independent slow tools — verify round latency drops from 3*T to ~T.
Hint
If you do not see the speedup, your gather is awaiting sequentially somewhere — usually a missed await or a serialized handler.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.