n4nAI

Parallel tool use with Claude's tool_choice parameter

Learn how to force Claude to call multiple tools in one turn using the tool_choice parameter, with runnable Python code for parallel execution.

n4n Team3 min read701 words

Audio narration

Coming soon — every post will get a voice note here.

Claude does not expose a dedicated “parallel” flag, but you can coerce claude tool_choice parallel tool use by setting tool_choice to {type: "any"} and designing independent tools. The model then returns multiple tool_use content blocks in a single assistant turn, letting your backend fan out work instead of waiting for sequential round trips. This how-to walks through the exact request shape, concurrent execution, and response handling you need to ship it.

Step 1: Define independent, side-effect-free tools

Parallel tool execution only works when the calls do not depend on each other’s outputs. If tool B needs the result of tool A, you must let Claude sequence them. For a true fan-out, define schemas that take all needed inputs directly from the prompt.

Below are two independent tools: one fetches weather, one fetches a stock quote. Neither references the other.

[
  {
    "name": "get_weather",
    "description": "Get current weather for a city",
    "input_schema": {
      "type": "object",
      "properties": {
        "city": {"type": "string"}
      },
      "required": ["city"]
    }
  },
  {
    "name": "get_stock_quote",
    "description": "Get latest price for a ticker",
    "input_schema": {
      "type": "object",
      "properties": {
        "ticker": {"type": "string"}
      },
      "required": ["ticker"]
    }
  }
]

Keep schemas strict. Loosely typed inputs increase the chance Claude emits a malformed block and wastes a turn.

Step 2: Send the initial request with tool_choice forcing tool use

The tool_choice parameter controls whether Claude may call tools. The default is auto, which lets it answer textually. To force claude tool_choice parallel tool use, set type to any. This requires at least one tool call; with independent tools and a broad prompt, Claude typically emits two or more tool_use blocks.

import anthropic

client = anthropic.Anthropic()

response = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    tools=[
        {
            "name": "get_weather",
            "description": "Get current weather for a city",
            "input_schema": {
                "type": "object",
                "properties": {"city": {"type": "string"}},
                "required": ["city"],
            },
        },
        {
            "name": "get_stock_quote",
            "description": "Get latest price for a ticker",
            "input_schema": {
                "type": "object",
                "properties": {"ticker": {"type": "string"}},
                "required": ["ticker"],
            },
        },
    ],
    tool_choice={"type": "any"},
    messages=[
        {
            "role": "user",
            "content": "What's the weather in Tokyo and the price of NVDA?",
        }
    ],
)

print(response.stop_reason)  # expect "tool_use"

If you need to force a specific tool but still allow others alongside it, tool_choice also accepts {"type": "tool", "name": "get_weather"}. However, that restricts the primary call and is not the cleanest path for fan-out. Use any for general claude tool_choice parallel tool use.

Step 3: Extract every tool_use block from the response

Claude returns a content array. Each tool_use block has a unique id, a name, and input. You must collect all of them before executing.

tool_calls = []
for block in response.content:
    if block.type == "tool_use":
        tool_calls.append({
            "id": block.id,
            "name": block.name,
            "input": block.input,
        })

assert len(tool_calls) >= 1, "tool_choice=any should yield at least one call"
print(f"Claude requested {len(tool_calls)} tool call(s) in one turn")

When the prompt clearly maps to multiple independent actions, len(tool_calls) is often 2. That is the signal your parallel path is live.

Step 4: Run the tools concurrently

Do not loop serially. Use asyncio or a thread pool to execute the functions simultaneously. The example below uses asyncio.gather.

import asyncio
import time

async def get_weather(city: str) -> dict:
    await asyncio.sleep(0.2)  # simulate I/O
    return {"city": city, "temp_c": 21}

async def get_stock_quote(ticker: str) -> dict:
    await asyncio.sleep(0.2)
    return {"ticker": ticker, "price": 123.45}

async def execute_calls(tool_calls):
    start = time.monotonic()
    tasks = []
    for call in tool_calls:
        if call["name"] == "get_weather":
            tasks.append(get_weather(call["input"]["city"]))
        elif call["name"] == "get_stock_quote":
            tasks.append(get_stock_quote(call["input"]["ticker"]))
    results = await asyncio.gather(*tasks)
    elapsed = time.monotonic() - start
    print(f"Executed {len(tasks)} tools in {elapsed:.2f}s (parallel if ~0.2s)")
    return results

results = asyncio.run(execute_calls(tool_calls))

If you are on a sync framework, swap in concurrent.futures.ThreadPoolExecutor. The key is that the wall-clock time for N independent tools should approximate the slowest single call, not their sum.

Step 5: Pack results back into a single user message

Anthropic requires that every tool_use from the assistant be answered with a tool_result in the next user message. You return a content array containing one tool_result block per call, referencing the original tool_use_id.

user_content = []
for call, result in zip(tool_calls, results):
    user_content.append({
        "type": "tool_result",
        "tool_use_id": call["id"],
        "content": str(result),
    })

followup = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    tools=[...],  # same tools as before
    messages=[
        {"role": "user", "content": "What's the weather in Tokyo and the price of NVDA?"},
        {"role": "assistant", "content": response.content},
        {"role": "user", "content": user_content},
    ],
)

print(followup.content[0].text)

Omitting even one tool_use_id triggers a 400. Build the list programmatically to avoid drift.

Step 6: Confirm Claude actually went parallel

Verification is straightforward. In your execution layer, log the start timestamp of each tool invocation. If all timestamps fall within a few milliseconds, you have parallel execution. Additionally, assert on the assistant message:

def verify_parallel(resp):
    ids = [b.id for b in resp.content if b.type == "tool_use"]
    if len(ids) > 1:
        print("SUCCESS: claude tool_choice parallel tool use confirmed")
        return True
    print("NOTE: only one tool call emitted; prompt may not be parallel-friendly")
    return False

Run the script end-to-end. A successful run prints the parallel confirmation and the final synthesized answer from Claude that combines weather and stock data without an extra assistant round trip between the calls.

Step 7: Deal with errors without breaking the turn

One tool may fail while others succeed. Anthropic lets you return an error inside tool_result via is_error=True. Claude then adapts its final answer.

user_content.append({
    "type": "tool_result",
    "tool_use_id": failed_call["id"],
    "content": "upstream timeout",
    "is_error": True,
})

Do not raise an exception in your executor before building the result list. Catch, mark the block, and continue. The model is competent at saying “I couldn’t get the stock quote” while still reporting the weather.

Step 8: Cache tool definitions and control token cost

Tool schemas are repeated on every request. If your tool list is large and stable, attach cache_control to the tools or a system prefix to use prompt caching. This is independent of parallel calling but matters in production.

response = client.messages.create(
    model="claude-3-5-sonnet-20241022",
    max_tokens=1024,
    system=[
        {"type": "text", "text": "You are a finance and travel assistant."},
        {"type": "text", "text": "Use tools when needed.", "cache_control": {"type": "ephemeral"}}
    ],
    tools=tools,
    tool_choice={"type": "any"},
    messages=messages,
)

When you combine cached schemas with forced claude tool_choice parallel tool use, you get low-latency fan-out at predictable token cost.

Step 9: Know when not to force parallel

tool_choice: {type: "any"} is a blunt instrument. If the user asks a single simple question, forcing a tool call wastes tokens and can produce dummy invocations. Gate the forcing logic: use any only when you have detected multiple intents, or expose a higher-level router that decides between auto and any. In multi-step agents, alternate between auto (let Claude think) and any (force action) based on state.

The pattern above is the backbone for any agent that needs to hit several APIs at once. Get the request shape right, execute concurrently, and return all results in one message—Claude handles the synthesis.

Tagsclaudetool-choiceparallel-function-callingtool-use

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All parallel & multi-step tool use posts →