How to Fix 'duplicate tool calls in production' in CrewAI (Python)

By Cyprian AaronsUpdated 2026-04-21
duplicate-tool-calls-in-productioncrewaipython

What the error means

duplicate tool calls in production usually means CrewAI saw the same tool invocation more than once in a single run, or it detected a repeated call pattern that it treats as unsafe. In practice, this shows up when an agent retries, a task is executed twice, or your own code re-registers tools/agents on every request.

You’ll usually hit it in API servers, FastAPI apps, background workers, or notebook-to-service migrations where the same Crew gets constructed and kicked off multiple times.

The Most Common Cause

The #1 cause is reusing mutable CrewAI objects across requests and accidentally calling .kickoff() more than once on the same Crew, Agent, or tool-wrapped execution path.

A common broken pattern is building the crew at module scope and then invoking it per request. That looks clean, but in production it often causes repeated tool registration or repeated execution state.

Broken vs fixed

Broken patternFixed pattern
Crew created once at import timeCrew created inside a factory per request
Shared mutable agent/tool stateFresh Agent and Crew instances each run
Same tool-call state reusedIsolated execution context
# broken.py
from crewai import Agent, Task, Crew
from crewai_tools import SerperDevTool

search_tool = SerperDevTool()

researcher = Agent(
    role="Researcher",
    goal="Find company info",
    backstory="You research companies.",
    tools=[search_tool],
)

task = Task(
    description="Search for ACME Inc.",
    expected_output="A short summary",
    agent=researcher,
)

crew = Crew(
    agents=[researcher],
    tasks=[task],
)

# In FastAPI / Flask / worker code this may run multiple times
result = crew.kickoff()
print(result)
# fixed.py
from crewai import Agent, Task, Crew
from crewai_tools import SerperDevTool

def build_crew():
    search_tool = SerperDevTool()

    researcher = Agent(
        role="Researcher",
        goal="Find company info",
        backstory="You research companies.",
        tools=[search_tool],
    )

    task = Task(
        description="Search for ACME Inc.",
        expected_output="A short summary",
        agent=researcher,
    )

    return Crew(
        agents=[researcher],
        tasks=[task],
    )

def run():
    crew = build_crew()
    return crew.kickoff()

result = run()
print(result)

If you’re exposing this through an API, build the crew inside the request handler or inside a factory function. Don’t keep one global Crew object around and assume it’s stateless.

Other Possible Causes

1) The same tool is attached twice

This happens when you add a tool in two places: directly on the agent and again through a helper that mutates the agent later.

tool = SerperDevTool()

agent = Agent(..., tools=[tool])
agent.tools.append(tool)  # duplicate registration

Fix it by assigning tools once:

agent = Agent(..., tools=[tool])

2) The task is executed twice by your app layer

A retry middleware, webhook replay, or double HTTP submit can call .kickoff() twice even though your code looks fine.

@app.post("/run")
def run_crew():
    crew = build_crew()
    return crew.kickoff()

# If client retries or load balancer replays request,
# your tool call can happen twice.

Add idempotency at the API boundary:

request_id = payload["request_id"]
if already_processed(request_id):
    return cached_result(request_id)

3) You are mixing sequential and nested execution incorrectly

If one task triggers another task that uses the same agent/tool set, CrewAI can interpret that as duplicate execution.

# risky pattern
crew = Crew(
    agents=[researcher],
    tasks=[task1, task2],  # both tasks call same tool path repeatedly
)

Split responsibilities:

research_crew = Crew(agents=[researcher], tasks=[task1])
summary_crew = Crew(agents=[writer], tasks=[task2])

4) Your custom tool is not idempotent

If your tool writes to a DB, queue, or ticketing system without deduplication, a retry can look like a duplicate tool call from the outside.

class CreateTicketTool(BaseTool):
    name = "create_ticket"

    def _run(self, title: str):
        # broken: no dedupe key
        return ticketing_api.create(title=title)

Use an external idempotency key:

class CreateTicketTool(BaseTool):
    name = "create_ticket"

    def _run(self, title: str, request_id: str):
        return ticketing_api.create(title=title, idempotency_key=request_id)

How to Debug It

  1. Log every kickoff

    • Print a unique request ID before crew.kickoff().
    • If you see two logs for one user action, the issue is above CrewAI.
  2. Inspect tool attachment

    • Log agent.tools before running.
    • Look for duplicates of the same class name or instance.
  3. Check whether your app retries

    • Search for Celery retries, HTTP client retries, webhook redelivery, or job requeue logic.
    • A second execution often looks like a “CrewAI bug” but isn’t.
  4. Reduce to one agent and one tool

    • Strip the workflow down to a single Agent, single Task, single BaseTool.
    • If the error disappears, add components back until duplication returns.

Example debug snippet:

print("request_id=", request_id)
print("tools=", [type(t).__name__ for t in researcher.tools])

crew = build_crew()
result = crew.kickoff()
print("done request_id=", request_id)

Prevention

  • Build crews with factory functions instead of global singletons.
  • Add idempotency keys to any custom tool that writes data.
  • Treat API handlers and workers as retryable by default; design for duplicate-safe execution.
  • Keep one source of truth for tool registration. Don’t append tools in multiple layers.
  • When debugging production incidents, start with request logs before blaming CrewAI internals.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides