Two tool calls can arrive in the same agent turn and still be an unsafe batch. If one call depends on the other, if both change the same record, or if the second call must observe the first call's effect, Promise.all turns a business rule into a race.

The practical rule is simple: parallelize only calls that are independent and do not share mutable state. Put a runtime barrier in front of dependencies, effects, and resources that require ordering. A provider setting may allow several calls, but your application still owns the execution decision.

This is narrower than orchestrating multiple coding agents in TypeScript. The question here is one turn that contains several tools, not how to split work across agents.

Diagram of an AI agent parallelizing independent reads and serializing calls that share state.

The decision: independent or dependent?

Before running a batch, classify each tool by what it reads, what it changes, and which result it needs. The model may suggest the list, but it should not be the only source of that classification.

Situation Starting policy Why
get_customer and get_inventory read different resources Parallelize Neither needs the other's result or effect.
reserve_stock and create_order change the same order Serialize or use a transaction Ordering and atomicity are part of the effect.
search_customer must provide an ID to get_balance Serialize The second call has no complete input yet.
Two tools write to the same file or document Serialize by resource Different arguments can still observe shared state.
send_email and charge_card are independent but irreversible Do not parallelize by default A partial failure can leave hard-to-compensate effects.

The Anthropic guide to parallel tool use recommends parallel calls for independent operations and shows how to disable them when ordering matters. The AutoGen agents guide makes the same distinction: parallelism helps with simultaneous tools, but should be disabled when side effects or shared state can interfere.

A provider flag is not your system policy

In the OpenAI Responses API create reference, parallel_tool_calls controls whether the model may run tool calls in parallel. In Anthropic's API, disable_parallel_tool_use inside tool_choice limits emission to one call in supported modes. Both controls are useful, but they sit at the provider boundary.

They do not know that two internal tools write the same order, that an approval covers only the first change, or that a worker resumed a step after a timeout. Even with the flag enabled, your scheduler must validate the received batch. Even with the flag disabled, two workers or queue deliveries can create concurrency later.

Put the decision in a tool registry

Keep execution metadata beside the schema so each handler does not invent its own rule:

type ToolMeta = {
  mode: "read" | "write";
  resource: string;
  dependsOn?: string[];
};

const tools: Record<string, ToolMeta> = {
  get_customer: { mode: "read", resource: "customer" },
  get_inventory: { mode: "read", resource: "inventory" },
  reserve_stock: { mode: "write", resource: "order" },
  create_order: { mode: "write", resource: "order", dependsOn: ["reserve_stock"] },
};

function canShareBatch(aName: string, a: ToolMeta, bName: string, b: ToolMeta) {
  return a.mode === "read" &&
    b.mode === "read" &&
    a.resource !== b.resource &&
    !a.dependsOn?.includes(bName) &&
    !b.dependsOn?.includes(aName);
}

This is illustrative. Production code still needs argument validation, authorization, and a complete dependency graph. The important boundary is where the decision lives. The runtime knows the real tool, protected resource, and dependency edges; the prompt only knows the description it received.

In practice, build independent batches and run each batch with Promise.all. When a call has a dependency, wait for the previous batch, update the context, and prepare the next call. For writes, use a transaction, a per-resource queue, or an idempotency key. Parallelism does not replace database concurrency control.

Correlate results by ID, never by position

A parallel response can finish in a different order from the order in which calls were emitted. Every call needs a stable ID, and every result must be attached to that ID. The Anthropic documentation describes returning results in one turn and using tool_use_id to associate each result with its call.

Do not rely on array positions:

for (const [index, result] of results.entries()) {
  run.toolResults[calls[index].name] = result;
}

If a call fails, is cancelled, or finishes later, the index is no longer a reliable identity. Prefer the call ID:

for (const result of results) {
  run.toolResults[result.callId] = {
    status: result.ok ? "ok" : "error",
    value: result.value,
  };
}

When a dependency cannot run, return an explicit error to the agent or stop the turn. Do not invent an empty result to fill a slot. The error should identify the blocked call, the missing dependency, and whether any effect was already confirmed.

Test the policy without accidental timing

Create a fixture with two reads that wait on a manual barrier. Release both together and assert that the scheduler started both. Then use two writes for the same resource, release the first before the second, and assert that the second did not start early. Finally, make a dependent call fail and verify that the next effect was not executed.

The minimum cases are:

  • two independent reads share a batch;
  • two writes for the same resource are serialized;
  • a call that needs an ID waits for the earlier result;
  • out-of-order results remain attached to the correct callId;
  • an unknown dependency or missing version fails before the external effect.

I ran a small deterministic local fixture with Node.js v25.5.0 on October 30, 2026. It used controlled promises and no model provider, database, or network. The result confirmed the five behaviors above. That proves only the fixture's logic, not latency or SDK security. For domain-specific races, combine this with deterministic JavaScript race-condition tests and tests against the database or queue that protect the real effect.

Common failures

Using Promise.all for every list. The model API does not know your domain invariants. Separate independent reads from mutations before creating promises.

Trusting only an instruction that says “do not call tools in parallel.” That reduces one model behavior, but it does not protect two workers, retries, callbacks, or queue redeliveries.

Serializing everything and calling it solved. It is a useful diagnostic, but it can add latency and hide which reads are independent. Classify the tools and reopen parallelism only with tests.

Correlating by tool name. Two calls to the same tool in one turn need different IDs. The name identifies a capability, not an occurrence.

Ignoring partial failure. If a read completed and a write failed, record the known state. Do not automatically repeat an effect just because the batch did not finish.

Checklist before enabling parallel tool calls

  • Does every tool declare whether it reads or changes state?
  • Does the runtime know the shared resource and dependency edges?
  • Does the scheduler build the batch instead of accepting unrestricted Promise.all?
  • Does every result carry the original call ID?
  • Do external effects have idempotency, a transaction, or a per-resource queue?
  • Is there a negative test for a missing dependency and partial failure?
  • Does tracing record the batch, order, blocks, and confirmed effects?
  • Is the provider setting treated as an emission optimization rather than business authorization?

Conclusion

Parallel tool calls are useful when independence is a property you can demonstrate. They are not a shortcut for running any list faster. Keep the policy in the runtime: classify tools, parallelize independent reads, serialize dependencies and shared state, and associate every response with its call ID.

If the batch belongs to a larger workflow, combine this policy with validating tool calls before the handler and verifying a tool effect before retrying. That keeps concurrency connected to validation and recovery instead of turning it into an isolated flag.

How this article was made

Samuel Fajreldines wrote this article from the repository's post and link inventory, current result inspection, and public documentation from OpenAI, Anthropic, and AutoGen. The independence, dependency, and shared-state matrix is original synthesis. The fixture is illustrative and was run locally with Node.js, without calling a model provider or production tool.

I used RemoteCode as the author's tool to organize research and review consistency. It did not run a production agent and does not replace validation in the reader's environment.

Sources consulted