Parallel Tool Calling
Execute multiple concurrent tool requests simultaneously using Promise.allSettled and rate-limited batch workers.
TL;DR
- Execute multi-tool responses concurrently using
Promise.allSettledexecution workers. - Throttle maximum simultaneous tool operations using the
p-limitlibrary. - Return individual response blocks mapped to each corresponding
tool_call_id.
Concurrent Dispatch Architecture
AllSettled WorkerExecute multiple tool calls in parallel with isolated error safety.
const results = await Promise.allSettled(
toolCalls.map(async call => ({
id: call.id,
data: await executeTool(call.name, call.args),
}))
);Result Block FormatterMap settled promise states into appropriate LLM tool response messages.
const msgs = results.map((r, i) => ({
role: 'tool',
tool_call_id: toolCalls[i].id,
content: r.status === 'fulfilled'
? JSON.stringify(r.value.data)
: JSON.stringify({ error: (r as any).reason }),
}));Concurrency ThrottlingConstrain maximum concurrent tool requests using p-limit.
import pLimit from 'p-limit';
const limit = pLimit(3); // Max 3 concurrent calls
const tasks = toolCalls
.map(c => limit(() => executeTool(c)));
const out = await Promise.allSettled(tasks);Partial Failure Recovery
Granular Error IsolationSegregate successful tool data from failed execution exceptions.
const payload = results.map(res => {
if (res.status === 'fulfilled') {
return { success: true, data: res.value };
}
return { success: false, error: 'Unavailable' };
});Model Guidance FeedbackPrompt model to synthesize partial answers from successful calls.
const feedback = {
role: 'tool',
tool_call_id: failedCall.id,
content: 'DB timeout. Use available results.',
};Deadlock Prevention CheckVerify that total tool responses exactly matches request count.
if (toolResponses.length !== toolCalls.length) {
throw new Error('Mismatched tool call IDs detected');
}Cross-Provider Concurrency
OpenAI Multi-CallExtract and execute multiple tool_calls from chat completion.
const calls = res.choices[0].message.tool_calls ?? [];
const toolOutputs = await Promise.all(
calls.map(c => dispatchOpenAiTool(c))
);Claude Multi-Tool ContentExtract multiple tool_use blocks from Anthropic message content.
const toolBlocks = res.content.filter(
b => b.type === 'tool_use'
);
const responses = await Promise.all(
toolBlocks.map(b => dispatchClaudeTool(b))
);Batch Turn ResumptionSend all parallel tool results back in a single follow-up message.
await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 1024,
messages: [...history, assistantMsg, {
role: 'user',
content: responses,
}],
});Performance Optimization
Batch Latency LoggingMeasure overall wall-clock time saved through parallel execution.
const t0 = performance.now();
await Promise.all(toolTasks);
const duration = performance.now() - t0;
metrics.timing('parallel_tool_duration_ms', duration);Deduplicated Tool CallsFilter identical tool arguments before dispatch to avoid waste.
const entries = calls
.map(c => [JSON.stringify(c.args), c]);
const unique = Array.from(new Map(entries).values());Sub-Request CacheServe repeated parallel tool queries from in-memory cache.
async function cachedExecute(call: ToolCall) {
const a = JSON.stringify(call.args);
const key = `tool:${call.name}:${a}`;
return cache.getOrSet(key, () => execute(call), 60);
}Tips
- Use
Promise.allSettledinstead ofPromise.allso that a single failed tool call does not abort other successful operations. - Include partial failure descriptions in error result blocks so models adapt using successful
tool-results.
Warnings
- Limit parallel tool execution concurrency using
p-limitto prevent exhausting downstream database connection pools and API quotas. - Ensure every single
tool_call_idreceives a response block even if an operation was rejected or skipped to prevent conversation deadlocks.
In Practice
Dispatches multiple tool calls in parallel with Promise.allSettled and returns matching responses.
- Simulate parallel tool calls emitted by AI model.
- Execute operations concurrently using Promise.allSettled.
- Format fulfilled and rejected states into valid tool messages.
- Verify every tool call received a corresponding response block.
type Call = { id: string; arg: string };
async function runCall(c: Call) {
if (c.arg === 'bad') throw new Error('Err');
return { sym: c.arg, price: 150 };
}
async function dispatchAll(calls: Call[]) {
const settled = await Promise.allSettled(
calls.map(c => runCall(c))
);
return settled.map((res, i) => ({
role: 'tool',
tool_call_id: calls[i].id,
content: res.status === 'fulfilled'
? JSON.stringify(res.value)
: JSON.stringify({ error: 'Failed' }),
}));
}
const reqs = [{ id: '1', arg: 'AAPL' }];
console.log(await dispatchAll(reqs));FAQ
When a user query involves multiple independent operations (e.g., 'Compare weather in Tokyo, London, and New York'), modern models like GPT-4o and Claude 3.5 Sonnet emit multiple tool calls in a single response turn.
Promise.all rejects immediately upon the first error, abandoning other pending tools. Promise.allSettled waits for all operations to finish, allowing you to return successful results alongside clear error messages for failed calls.
Use utility libraries like p-limit. Define a concurrency limit (e.g., 5 concurrent requests) and wrap your tool execution functions so outbound network calls do not overwhelm resources.