Do it all at once: parallel tool calls and how to build them
When an agent needs a few independent lookups, there's no reason to do them one at a time. Parallel tool calls, what the model is actually deciding, and the cheapest speed-up there is in an agent.
Most agents have to look a few things up before they can answer. Not one thing, several. And a lot of the time those lookups have nothing to do with each other: a record out of the database, a search result, a document pulled from somewhere else. The agent needs all of it before it can respond, but no single piece depends on any of the others.
The slow way to do that is in order. Fire off the first request, wait for it to come back, fire the second, wait again, then the third. Three trips out to the network, stacked one behind the other, each one sitting idle while the one before it finishes. The step ends up taking as long as all three added together, when it only ever needed to take as long as the slowest one. For a while I didn’t even clock it as a problem. The agent worked, it just felt sluggish, and the reason was sitting in the logs the whole time.
The fix has a name. Parallel tool calls.
Picking up from the loop
If you’ve read the ReAct piece, you know an agent runs a loop: a Thought, then an Action (one tool call), then an Observation, round and round. It’s easy to assume each pass does exactly one thing. One thought, one tool, one result.
Parallel tool calls is what happens when a single Action step fires off more than one tool call at the same time. The loop doesn’t change. What changes is how much can happen inside one turn of it. If the loop itself still feels fuzzy, go back to the ReAct piece first, because this sits right on top of it.
What it actually is
The idea is almost too simple to need a name. When an agent needs a few bits of information that don’t depend on each other, there’s no reason to go and get them one at a time. The database read doesn’t need anything from the search. The search doesn’t care about the document. So you send all three off together and pick the results up once they’re all back.
That’s it. The only thing standing between the slow version and the fast one is whether independent work waits in a queue or runs at the same time.

What the model actually does
Here’s the bit that caught me out the first time I looked properly. The parallelism isn’t something you build around the model. The model decides to do it.
In an ordinary turn, the model’s reply contains one tool call. In a parallel turn, that single reply contains several tool calls at once, packaged into the one message. The runtime sees there’s more than one, runs them at the same time instead of one after another, waits for all of them to land, and then hands every result back together as the observations for the next turn. The model reads the whole lot in one go and carries on from there.
So the call the model is making is really just one question: are these independent? If it reckons three lookups don’t rely on each other, it sends all three out together. If one of them needs the answer from another first - say it has to look up a user’s ID before it can pull that user’s orders - then it can’t send them together, because the second call doesn’t even exist yet until the first one comes back. In that case it goes one at a time, which is the right thing to do. Parallel calls only help when the work is genuinely independent, and it’s the model drawing that line, not you.

How to build it
There’s less to do here than you’d think, because most of the work is the model’s. With the Anthropic API you don’t ask for parallel calls directly. You define your tools clearly, and the model comes back with several tool_use blocks in one response whenever it judges the calls to be independent. The part that’s on you is noticing there’s more than one, running them at the same time rather than looping through them, and sending all the results back together in the next message.
The loop is the same one from the ReAct article - the full runnable file lives in the companion repo. The agent goes async (AsyncAnthropic, an async run_tool), and the tool-execution block becomes an asyncio.gather - the same pattern Anthropic’s quickstart agent uses. That’s the whole change, and the snippet below is the only part of the loop that’s different. Watch [STEP 2] of the run: two check_calendar calls land inside a single assistant turn, run concurrently, and feed back as one batch of observations - the [ELAPSED] line shows the pair finishing in the time of one. The steps either side of it stay sequential, because their work is dependent: nothing can run before the emails exist, and the event can’t be created until both calendars are in.
async def execute_tool(tool_use):
"""Run one tool call and wrap it as a tool_result block."""
result, is_error = await run_tool(tool_use.name, tool_use.input)
print(f"[RESULT]{' (error)' if is_error else ''} {tool_use.name} → {json.dumps(result)}")
return {
"type": "tool_result",
"tool_use_id": tool_use.id,
"content": json.dumps(result),
"is_error": is_error,
}
# inside the loop
for tool_use in tool_calls:
print(f"[TOOL] {tool_use.name}({json.dumps(tool_use.input)})")
# Run every tool call in this turn CONCURRENTLY. gather() preserves
# order, so each tool_result lines up with its tool_use - and all
# results still go back in ONE user message.
started = time.perf_counter()
tool_results = await asyncio.gather(*(execute_tool(tu) for tu in tool_calls))
print(f"[ELAPSED] {len(tool_calls)} tool call(s) in {time.perf_counter() - started:.2f}s")
messages.append({"role": "user", "content": list(tool_results)}) [SYSTEM] You are a personal assistant who manages the user's calendar. Today's date is 2026-07-09.
[USER] Create me a meeting event with James and Sarah please for next Wednesday at 2pm for 1 hour.
[STEP 1]
[THINKING] The user wants to create a meeting with James and Sarah for next Wednesday (2026-07-15) at 2pm for 1 hour. I need to first look up their emails, then check their calendars, then create the event.
Let me start by listing the emails.
[CLAUDE] Sure! Let me first look up James and Sarah's emails right away!
[TOOL] list_emails({})
[RESULT] list_emails → {"James Jones": "james@abc.com", "Sarah Smith": "sarah@abc.com", "Trish Taylor": "trish@abc.com"}
[ELAPSED] 1 tool call(s) in 1.00s
[STEP 2]
[CLAUDE] Got their emails! Now let me check both of their calendars at the same time to make sure they're free!
[TOOL] check_calendar({"email": "james@abc.com"})
[TOOL] check_calendar({"email": "sarah@abc.com"})
[RESULT] check_calendar → [{"title": "Monthly Review", "start": "2026-07-13T10:00:00Z", "end": "2026-07-13T11:00:00Z"}, {"title": "1-on-1 with Manager", "start": "2026-07-16T09:00:00Z", "end": "2026-07-16T09:45:00Z"}]
[RESULT] check_calendar → [{"title": "Marketing Brainstorm", "start": "2026-07-15T10:30:00Z", "end": "2026-07-15T12:00:00Z"}, {"title": "Client Onboarding", "start": "2026-07-17T13:00:00Z", "end": "2026-07-17T14:00:00Z"}]
[ELAPSED] 2 tool call(s) in 1.00s
[STEP 3]
[THINKING] Next Wednesday is 2026-07-15. The meeting is at 2pm for 1 hour, so 2026-07-15T14:00:00 to 2026-07-15T15:00:00.
Let me check both calendars:
- James: Monthly Review on July 13, 1-on-1 with Manager on July 16. Nothing on July 15 at 2pm. ✓
- Sarah: Marketing Brainstorm on July 15 from 10:30-12:00. Nothing at 2pm on July 15. ✓
Both are free at 2pm on July 15, so I'll go ahead and create the event.
[CLAUDE] Both James and Sarah are free next Wednesday (July 15th) at 2pm. Let me go ahead and create the event!
[TOOL] create_calendar_event({"title": "Meeting", "start": "2026-07-15T14:00:00", "end": "2026-07-15T15:00:00", "attendees": ["james@abc.com", "sarah@abc.com"]})
[RESULT] create_calendar_event → {"event_id": "evnt_123", "status": "success"}
[ELAPSED] 1 tool call(s) in 1.00s
[FINAL]
[CLAUDE] All done! Here's a summary of the event that's been created:
- 📅 **Date:** Wednesday, July 15th, 2026
- ⏰ **Time:** 2:00 PM – 3:00 PM
- 👥 **Attendees:** James Jones & Sarah Smith
Both attendees had no conflicts at that time. Let me know if you need any changes! Why it’s worth doing
Two reasons, both of them concrete. The first is latency. Three independent calls run together take about as long as the slowest one, not the sum of all three - it’s right there in the trace above, where two 1-second calendar checks finish in 1.00s flat. On an agent that makes a handful of lookups per task, that’s the gap between something that feels quick and something that feels like it’s grinding. The second is that you get it for almost nothing. You’re not writing some clever orchestration layer. You’re defining tools well enough that the model can tell what’s independent, and then running what it gives you at the same time instead of in a loop.
It’s about the cheapest speed-up there is in an agent. The model does most of the thinking. You just have to stop putting independent work in a queue.