Home ยป Tool-Use Loops: Why the Model Keeps Calling the Same Tool
Tool-Use Loops: Why the Model Keeps Calling the Same Tool

Tool-Use Loops: Why the Model Keeps Calling the Same Tool

Figures on this page were verified 18 October 2026 against the providers' own documentation. Pricing, context windows and rate limits change without notice, so confirm any number against the provider before you rely on it. Tell us if something here is out of date.

A model stuck calling the same tool repeatedly is almost always reacting correctly to a tool result that never changes, not malfunctioning on its own. From the model’s side, each loop iteration looks like a fresh decision: it sees the conversation so far, including the tool result it just received, and decides whether the task is complete. If that result does not look different or does not look like progress, calling the tool again is often the locally reasonable choice.

There is no built-in loop protection

Each request to the API is independent, so nothing in the API counts how many turns your code has run or how many times the same tool has been called. Every one of those limits is something you enforce in your own orchestration loop. If you built a simple while-loop that calls the model, runs whatever tool it asks for, and feeds the result back, nothing stops that from running until you hit a budget, a timeout, or context_length_exceeded from the accumulated history.

The three real causes

1. The tool result does not tell the model it is done

If a search tool returns the same ten results every time, or a status-check tool returns “pending” with no indication of how long pending typically lasts, the model has no signal to stop. It is not stuck; it is correctly inferring that the task is not yet finished.

# WRONG: no signal that repeating this call is pointless
{"status": "pending"}

# RIGHT: gives the model a reason to stop or change approach
{
  "status": "pending",
  "checked_count": 3,
  "note": "Status has been pending for 3 checks. Typical jobs complete within 1 check. Consider informing the user this may be delayed rather than checking again immediately."
}

2. The task genuinely cannot be completed with the tools given

If none of the available tools can actually satisfy the request, a model without a clear “give up” instruction will sometimes keep trying variations rather than reporting failure. This is a prompting gap, not a model defect: tell it explicitly that if a tool cannot produce the needed information after a reasonable number of attempts, it should say so rather than continue.

3. Your orchestration code has no exit condition

This is the most common cause in practice. The loop that drives tool calls was written to run until the model stops asking for tools, with no independent cap. That is fine until a model-side or tool-side quirk produces a cycle, at which point nothing in your code intervenes.

Enforce a hard limit in your own loop

This is not optional infrastructure, it is the fix. Cap both the total number of turns and repeated calls to the same tool with the same arguments.

MAX_TURNS = 8
MAX_SAME_CALL = 2

call_history = []  # list of (tool_name, args_json) tuples

for turn in range(MAX_TURNS):
    response = client.messages.create(model=MODEL, tools=TOOLS, messages=messages)
    if response.stop_reason != "tool_use":
        break  # model answered, done

    tool_block = next(b for b in response.content if b.type == "tool_use")
    call_key = (tool_block.name, json.dumps(tool_block.input, sort_keys=True))

    if call_history.count(call_key) >= MAX_SAME_CALL:
        # break the loop explicitly rather than running the tool again
        tool_result = "This exact call has already been made twice with no new result. Stop retrying and report what you know, or ask the user for guidance."
        is_error = True
    else:
        tool_result = run_tool(tool_block.name, tool_block.input)
        is_error = False

    call_history.append(call_key)
    messages.append({"role": "assistant", "content": response.content})
    messages.append({"role": "user", "content": [
        {"type": "tool_result", "tool_use_id": tool_block.id,
         "content": tool_result, "is_error": is_error}
    ]})
else:
    # MAX_TURNS exhausted without the model finishing
    log.warning("tool loop hit MAX_TURNS=%d, forcing stop", MAX_TURNS)

The key move is returning the repeat-detection as a tool_result with is_error: true, not just silently refusing to run the tool. That gives the model the information it needs to change strategy on its next turn, rather than leaving it to call the same tool again believing nothing happened.

When the repeated tool is itself rate limited

If the tool the model keeps calling wraps a rate-limited API, a loop soon turns into a stream of failures. Return the error with is_error set so the model can back off, and read 429 rate limits and how to back off correctly before you let a loop retry on its own.

Budget turns like you budget tokens

Each loop iteration resends the growing conversation history, so a stuck loop is not just slow, it is actively expensive: every repeated call re-sends every prior tool result as input tokens, discounted only if prompt caching applies. An 8-turn cap on a conversation with a 2,000-token system prompt and growing tool results is cheap insurance against a cycle that could otherwise run until the context window fills or the bill does.

Related errors

If the model is not calling a tool enough rather than too much, see the model ignored the tool and answered in prose. If the loop is being cut off by a provider-imposed ceiling rather than your own, see tool call limit reached in a single turn. The full map is in the LLM API error reference.

Frequently asked questions

Why does the model keep calling the same tool?

Usually because the tool result never changes or never signals completion, so another call looks like the sensible next step. It is reacting to what it sees, not malfunctioning.

Is there a built-in limit on tool-calling turns?

Each request is independent, so nothing in the API counts your turns or repeated calls. A turn cap and a same-call cap have to live in your own orchestration loop.

How do I make the model stop retrying?

Return the repeat as a tool result with is_error set to true and a clear message, rather than silently refusing to run the tool. That gives the model the information it needs to change approach.

Chirag Darji

Chirag Darji is the founder of VGraple and the editor of It's About You. He writes about the LLM APIs and developer tooling he works with, and every figure published here is checked against the provider's own documentation before it goes live, with the date it was verified shown on the page.

More Reading

Post navigation