Parallel Calls
On this page
Multiple Tools in a Single Response
One of Grok function calling’s most powerful features is the ability to return multiple tool calls in a single response. Instead of making a round trip for each function, the model identifies all the information it needs and generates every call in parallel.
The parallel_tool_calls Parameter
Parallel calls are enabled by default. The parallel_tool_calls parameter controls this behavior:
# Enabled by default (no need to specify)
response = client.chat.completions.create(
model="grok-4.5",
messages=messages,
tools=tools,
parallel_tool_calls=True # This is the default
)
When a user asks, “What’s the weather in Paris and Lyon?”, the model returns two tool_calls in the same response:
{
"tool_calls": [
{
"id": "call_001",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"Paris\"}"
}
},
{
"id": "call_002",
"function": {
"name": "get_weather",
"arguments": "{\"city\": \"Lyon\"}"
}
}
]
}
Handling Parallel Calls
Your code must handle an array of tool_calls, not just one:
import json
import asyncio
message = response.choices[0].message
if message.tool_calls:
# Execute all functions
tool_results = []
for tool_call in message.tool_calls:
args = json.loads(tool_call.function.arguments)
result = execute_function(tool_call.function.name, args)
tool_results.append({
"role": "tool",
"tool_call_id": tool_call.id,
"content": json.dumps(result)
})
# Send ALL results back to the model
messages = [
{"role": "user", "content": "What's the weather in Paris and Lyon?"},
message,
*tool_results # All results
]
final = client.chat.completions.create(
model="grok-4.5",
messages=messages,
tools=tools
)
Critical point: you must return all tool results, not just the first one. Each result is linked to its corresponding tool_call_id.
True Parallel Execution on the Client
The model returns calls in parallel, but your code must execute them in parallel. In Python, use asyncio or concurrent.futures:
import concurrent.futures
def execute_parallel(tool_calls):
results = []
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {}
for tc in tool_calls:
args = json.loads(tc.function.arguments)
future = executor.submit(execute_function, tc.function.name, args)
futures[future] = tc.id
for future in concurrent.futures.as_completed(futures):
call_id = futures[future]
result = future.result()
results.append({
"role": "tool",
"tool_call_id": call_id,
"content": json.dumps(result)
})
return results
Parallel execution significantly reduces total latency. If each API call takes 200ms, two sequential calls = 400ms, but two parallel calls = ~200ms.
Disabling Parallel Calls
In some cases, you will want sequential calls:
response = client.chat.completions.create(
model="grok-4.5",
messages=messages,
tools=tools,
parallel_tool_calls=False
)
Disable parallelism whenever call independence isn’t guaranteed. The most obvious case is data dependency: if one tool’s result feeds into the next tool’s arguments—looking up a customer ID before retrieving their orders—running them in parallel will at best cause an error, and at worst produce a silently incorrect result. Business logic order imposes the same constraint: creating a user before assigning them a role is non-negotiable, and only sequential mode guarantees this. Two more practical reasons complete the picture: an external API subject to concurrent request limits won’t handle a burst of parallel calls well, and during development, sequential mode makes tracking calls infinitely more readable—one call, one result, one log, in order.
Parallel Calls with Different Tools
The model can also call different tools in parallel:
# Request: "Book a table and tell me the weather tonight"
# The model returns:
# - tool_call_1 : book_table(restaurant="Le Petit Bistrot", time="20:00")
# - tool_call_2 : get_weather(city="Paris", time="tonight")
This is particularly useful for versatile assistants that need to gather information from different sources.
Error Handling in Parallel
When one call fails but others succeed, still return all results. The model handles individual errors:
results = []
for tc in message.tool_calls:
try:
result = execute_function(tc.function.name, args)
results.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps(result)
})
except Exception as e:
results.append({
"role": "tool",
"tool_call_id": tc.id,
"content": json.dumps({"error": str(e)})
})
Key Takeaways
- Parallel calls are enabled by default (
parallel_tool_calls: true) - The model returns multiple
tool_callsin a single response - You must return all results, each linked to its
tool_call_id - Implement client-side parallel execution to reduce latency
- Disable parallelism when there are dependencies between calls
- Handle errors individually—do not block everything for a single failure