n4nAI

How parallel tool calls work in the OpenAI API

Learn how parallel tool calls work in the OpenAI API with a step-by-step guide: define tools, send requests, run calls concurrently, and verify.

n4n Team2 min read527 words

Audio narration

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

Understanding how parallel tool calls work in the OpenAI API saves you from serial latency and brittle orchestration code. The API lets a model emit multiple function calls in a single response, and your client must execute them concurrently and return results in one follow-up request.

Step 1: Define tool schemas that are independently executable

Parallelism only works when the functions have no data dependencies. If the model needs the output of get_user before calling get_orders, it will emit serial calls across turns. Design schemas that take all needed inputs as parameters.

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

Attach both to the tools array. Keep schemas strict—no free-form dicts—so the model fills arguments correctly without follow-up clarification.

Step 2: Send the request with parallel_tool_calls enabled

The OpenAI Chat Completions endpoint accepts parallel_tool_calls (default true). You can leave it implicit, but set it explicitly to document intent.

from openai import OpenAI

client = OpenAI()  # or point base_url at an OpenAI-compatible gateway

resp = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "What's the weather in Tokyo and the price of AAPL?"}],
    tools=tools,
    parallel_tool_calls=True,
)

If you route through an OpenAI-compatible gateway such as n4n.ai, the same request shape works and you get automatic fallback when a provider is rate-limited or degraded, without changing client code.

Raw curl equivalent:

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gpt-4o",
    "messages": [{"role": "user", "content": "What'\''s the weather in Tokyo and the price of AAPL?"}],
    "tools": [...],
    "parallel_tool_calls": true
  }'

Step 3: Parse the response for multiple tool_calls

The assistant message contains a tool_calls list. Each entry has a unique id, a function.name, and a JSON string in function.arguments.

msg = resp.choices[0].message
assert msg.tool_calls is not None
for tc in msg.tool_calls:
    print(tc.id, tc.function.name, tc.function.arguments)

A parallel response looks like:

{
  "id": "call_abc",
  "type": "function",
  "function": { "name": "get_weather", "arguments": "{\"city\":\"Tokyo\"}" }
}

and a second entry with get_stock_price. The model decided both are independent, so it batched them. To see how parallel tool calls work in the OpenAI API at the wire level, this list length is your signal.

Step 4: Execute the calls concurrently

Do not loop sequentially. Use asyncio or a thread pool. Below is a thread pool example that respects the call IDs.

import json
from concurrent.futures import ThreadPoolExecutor

def dispatch(tc):
    args = json.loads(tc.function.arguments)
    if tc.function.name == "get_weather":
        return tc.id, get_weather(**args)
    if tc.function.name == "get_stock_price":
        return tc.id, get_stock_price(**args)
    raise ValueError(f"unknown tool {tc.function.name}")

with ThreadPoolExecutor() as ex:
    results = list(ex.map(dispatch, msg.tool_calls))

get_weather and get_stock_price are your local stubs—HTTP calls, DB queries, whatever. Because they run in parallel, total latency is the max of the two, not the sum.

Step 5: Return results as distinct tool messages

You must echo the assistant message with its tool_calls, then append one tool message per call using the exact tool_call_id.

messages = [
    {"role": "user", "content": "What's the weather in Tokyo and the price of AAPL?"},
    msg,  # assistant message with tool_calls
]
for tc_id, result in results:
    messages.append({
        "role": "tool",
        "tool_call_id": tc_id,
        "content": json.dumps(result),
    })

followup = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools,
)

The API requires a 1:1 mapping between tool_calls and tool messages. Missing or mismatched IDs cause a 400.

Step 6: Handle the final response and verify success

The second response should contain a normal assistant message with content and no tool_calls.

final = followup.choices[0].message
if final.tool_calls:
    raise RuntimeError("model still wants tools")
print(final.content)

Understanding how parallel tool calls work in the OpenAI API means verifying concurrent execution end to end.

Verification checklist

  • The first response had len(msg.tool_calls) > 1.
  • Your execution layer launched concurrent tasks (confirm with logs or timing).
  • The follow-up request included one tool message per tool_call_id.
  • The final message contains user-facing text that references both pieces of data.
  • End-to-end latency is closer to the slowest tool than the sum of both.

Gotchas that break parallelism

Setting parallel_tool_calls=False forces the model to emit one call per turn. Use it when your tools mutate shared state.

If a tool fails, return an error string in the tool message content; the model can recover. Do not drop the message.

Some models ignore the flag if the prompt implies dependency. Write queries that clearly separate concerns.

When you need provider redundancy, an OpenAI-compatible endpoint that honors client routing directives simplifies failover. The request shape stays identical.

That’s the whole flow. Implement the concurrent dispatch once, and the rest is message bookkeeping.

Tagsopenaiparallel-tool-callsfunction-callingapi

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 →