Why web search never runs your code but your own tool always does — with identical syntax
Declaring a server-side tool like web search and a client-side tool that calls your own code looks identical, but only one of them ever hands execution back to your app.
It's a reasonable assumption to make: if you declare a tool the same way in the same file, it should behave the same way when it's called. That assumption breaks the first time you mix a server-side tool like web search with a client-side tool that calls your own code, and the difference matters if you're trying to reason about latency in your app.
Same declaration, different behavior
Declaring a tool feels uniform. You list it, the model can call it, and it's natural to expect every call to round-trip back through your own code the same way: the model emits a call, your code runs it, you send the result back. That pattern holds for tools you write yourself. It does not hold for web search.
The anchor case: serverTools vs. tools
The concrete example is a .webSearch(maxUses: 5) declared in serverTools sitting right next to a Swift function called lookupFavorites() declared in tools, in the same file, using the same declaration style. Despite that identical syntax, web search runs on Anthropic's own servers and its results land inside that same turn. lookupFavorites() triggers a tool-use block that only your own app can act on.
Why: who can execute the tool
The round-trip count comes down to a single question: who can actually execute the tool. Web search needs a live index and sandbox infrastructure that Anthropic already runs, so Anthropic finishes the call itself, inside the same turn. lookupFavorites() is arbitrary code that only exists on your device, so only your app can run it, which forces the model to stop and wait for a second request no matter how simple that function is.
Tested both ways
Sending a prompt like "search Osmosis, then check my notes" makes the split concrete. The search resolves inside that same request, with no callback into your own code required. The notes check can't resolve that way: the model emits the tool-use call, your code has to run it, and a second request carries the answer back into the conversation. It doesn't matter how trivial the client-side function is, even a one-line function forces that round trip. And it doesn't matter how often the server-side tool fires, every call still resolves in-turn because Anthropic holds the execution the whole time.
The takeaway
It isn't the syntax that decides whether a tool call round-trips, it's who can execute it. Anthropic runs its own tools inside the turn because it owns the infrastructure those tools depend on. Only your app can run your own tools, so the model has to stop and come back for the answer every time, regardless of how the tool was declared.
Key takeaways
- Declaring a server-side tool and a client-side tool with identical syntax does not mean they behave the same way at runtime.
- Web search resolves inside the same turn because Anthropic owns the infrastructure (a live index and sandbox) needed to execute it.
- A client-side tool like
lookupFavorites()is arbitrary code that only your app can run, which always forces a second round trip. - Round-trip count depends on who can execute the tool, not on how trivial the function is or how often the tool fires.
- This split doesn't cover domain filtering,
maxUsesrate limiting, or the tool-result schema, and it isn't a verdict on which pattern is "better."
Try it yourself
In your own Claude app, take one tool declared as a server-side tool, like web search, and one declared as a client-side tool that calls your own code. Walk through what happens at the network level for each: how many round trips occur, who executes the tool, and where the result enters the conversation. Running that comparison against your own tool mix is the fastest way to reason accurately about latency budgets in your app, and it's a useful mental model for anyone building with the Claude API through Humanitarians AI's Claude Basics series.
Chapters
Full transcript(auto-generated, with timestamps)
A tool call should always round-trip through my code?
[0:00]Someone assumes a tool call always comes back through their own code. Wrong guess. Sometimes it does, sometimes it doesn't. Why web search never runs your code, but your own tool always does.
Same declaration, different behavior
[0:10]Declaring a tool feels uniform. You list it, Claude can call it, so every call should behave the same way afterward. The natural guess, the model emits a call, your code runs it, you send the result back. That's true for your own tools. It is not true for web search. Here's the concrete case. Web search Max
The anchor: serverTools vs. tools
[0:26]Uses five sits in server tools, a Swift function called lookup favorites sits in tools, same file, same declaration style. Web search runs on Anthropic servers, and its results land inside that same turn. Lookup favorites triggers a tool use block that only your own app can act on.
Why: who can execute
[0:43]Round trip count comes down to who can execute. Web search needs a live index and a sandbox infrastructure Anthropic already runs, so it finishes the call itself in that same turn. Lookup favorites is arbitrary code on your device. Only your app can run it, which forces the model to stop and wait for a second request. Say to Claude, "Search
The anchor returns: tested both ways
[1:01]Osmosis, then check my notes." The search resolves inside that request, no callback into your code. Check notes can't. The model emits the call, your code runs it, and a second request carries the answer back. It doesn't matter how trivial your function is, even one line forces that round trip. And it doesn't matter how often web search fires, every call still resolves in turn because Anthropic still holds the execution. It isn't the syntax that
Carry-out
[1:23]Decides the round trip, it's who can execute. Anthropic runs its own tools inside the turn. Only you can run yours, so the model has to stop and come back for the answer. Your turn, here's the
Your turn
[1:35]Prompt, read it with me. In my Claude app, I have one tool declared as a server-side tool like web search, and one declared as a client-side tool that calls my own code. Walk me through what happens at the network level for each. How many round trips? Who executes the tool? And where the result enters the conversation, so I can reason about latency budgets in my app. Layem in for bear. Why web search never runs your
Outro
[1:56]Code, but your own tool always does with identical syntax. Lay 'em in for bear.
More from HAI
1:55Computer Use: Demo to Production.
1:49Why the same API key that shipped your prototype becomes a critical bug in production
2:13The model says "I have no preferences" while assigning 74% to survival.
4:10When AI Agents Disagree: Part 1 — The Mechanics of Cross-Agent Validation in Mycroft
1:39Persisting Progress Across Context Windows
4:44