How to Fix 'duplicate tool calls during development' in LangChain (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
duplicate-tool-calls-during-developmentlangchaintypescript

If you’re seeing duplicate tool calls during development, it usually means your agent is emitting the same tool invocation more than once before the first one has finished. In LangChain TypeScript, this shows up most often when you wire an agent loop, streaming handler, or React dev environment in a way that re-runs the same request path twice.

The fix is usually not in the model prompt. It’s in your execution flow: one request in, one agent run out, one tool call handled once.

The Most Common Cause

The #1 cause is double execution from React 18 Strict Mode, hot reload, or an effect that fires twice during development. The result is two identical AgentExecutor.invoke() calls, which produces duplicate ToolMessage entries and repeated tool calls.

Here’s the broken pattern:

BrokenFixed
Runs on every render/effect re-runGuards execution so it runs once
No idempotency around agent invocationSingle entry point with stable trigger
// ❌ Broken: this can run twice in dev
import { useEffect } from "react";
import { AgentExecutor } from "langchain/agents";

export function ChatPanel({ executor }: { executor: AgentExecutor }) {
  useEffect(() => {
    executor.invoke({
      input: "Check account balance and summarize it",
    });
  }, [executor]);

  return null;
}
// ✅ Fixed: guard against duplicate dev execution
import { useEffect, useRef } from "react";
import { AgentExecutor } from "langchain/agents";

export function ChatPanel({ executor }: { executor: AgentExecutor }) {
  const ran = useRef(false);

  useEffect(() => {
    if (ran.current) return;
    ran.current = true;

    void executor.invoke({
      input: "Check account balance and summarize it",
    });
  }, [executor]);

  return null;
}

If you’re using React Strict Mode, remember that effects are intentionally double-invoked in development. That means any side-effectful agent call must be protected.

A better production pattern is to move the call behind a user action:

const onSubmit = async () => {
  const result = await executor.invoke({
    input: userMessage,
  });
  setMessages((prev) => [...prev, result]);
};

Other Possible Causes

1) You are appending tool messages manually and LangChain is also appending them

This happens when you store ToolMessage objects yourself and then pass them back into the next invoke() call. LangChain sees the prior tool response and may try to continue the same tool loop.

// ❌ Broken
const messages = [
  new HumanMessage("Look up policy status"),
  new AIMessage({
    content: "",
    tool_calls: [{ id: "call_1", name: "lookupPolicy", args: "{}" }],
  }),
  new ToolMessage({ tool_call_id: "call_1", content: "Active" }),
];

await agent.invoke({ messages });
// ✅ Fixed
const messages = [new HumanMessage("Look up policy status")];
const result = await agent.invoke({ messages });

Only persist conversation state you actually need. Let the agent runtime manage intermediate tool-call state unless you have a very specific reason not to.

2) Your tool is not idempotent

If the model retries or your UI resubmits, a non-idempotent tool will create duplicate side effects. This is common with ticket creation, payment initiation, or record updates.

// ❌ Broken
const createClaimTool = tool(async ({ claimId }) => {
  await db.claims.insert({ claimId });
  return `Created claim ${claimId}`;
});
// ✅ Fixed
const createClaimTool = tool(async ({ claimId }) => {
  const existing = await db.claims.findUnique({ where: { claimId } });
  if (existing) return `Claim ${claimId} already exists`;

  await db.claims.insert({ claimId });
  return `Created claim ${claimId}`;
});

For banking and insurance workflows, tools that mutate state should always have dedupe keys.

3) You’re streaming partial outputs into another agent run

A common mistake is to start a second invoke() when a streamed chunk looks like a final answer or a tool request. That creates overlapping runs with the same context.

// ❌ Broken
for await (const chunk of chain.stream(input)) {
  if (chunk.tool_calls?.length) {
    await chain.invoke(input);
  }
}
// ✅ Fixed
for await (const chunk of chain.stream(input)) {
  handleChunk(chunk);
}
// only invoke once per user turn

Streaming should observe one run, not trigger a new run unless you explicitly queue it.

4) Your event handler is registered twice

In Node servers with hot reload or Next.js route handlers, it’s easy to register listeners twice. Then one user message triggers two executions.

// ❌ Broken
app.post("/chat", async (req, res) => {
  const result = await executor.invoke({ input: req.body.message });
  res.json(result);
});

If this file gets loaded twice during development, your route may execute twice depending on how your server is wired.

Use stable server startup code and avoid registering handlers inside modules that get re-evaluated on HMR.

How to Debug It

  1. Log every entry into your agent boundary Add a request ID and print it before calling invoke(). If you see two logs for one click, the bug is outside LangChain.

    console.log("[agent] start", requestId);
    
  2. Inspect whether Strict Mode or HMR is involved If this only happens in development and disappears in production builds, suspect React Strict Mode or hot module replacement first.

  3. Turn on LangChain debug logging Enable verbose output so you can see repeated AIMessage.tool_calls or repeated ToolMessage creation.

    import { setVerbose } from "@langchain/core/utils/debug";
    setVerbose(true);
    
  4. Check whether your tool has side effects If the same external record gets created twice, make the tool idempotent and log its inputs before mutation.

Prevention

  • Keep one request path per user action.
  • Make all write tools idempotent with an external dedupe key.
  • Don’t persist intermediate tool_calls unless you know exactly how they’ll be replayed.
  • In React apps, treat Strict Mode as real behavior and guard side effects accordingly.
  • Add request IDs to every agent run so duplicate execution shows up immediately in logs.

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