How to Fix 'duplicate tool calls in production' in LangChain (TypeScript)
When LangChain throws Error: duplicate tool calls in production, it usually means the same tool invocation is being executed more than once for a single model turn. In practice, this shows up when you wire an agent loop incorrectly, retry a request without idempotency, or accidentally process the same streaming event twice.
This is not a model quality issue. It is almost always a control-flow bug in your TypeScript app.
The Most Common Cause
The #1 cause is calling agentExecutor.invoke() or agentExecutor.stream() more than once for the same user action, usually from both a UI event and a server handler, or from a retry wrapper that replays the whole agent run.
Here’s the broken pattern:
| Broken | Fixed |
|---|---|
| Re-invokes the agent on every render / event replay | Guards execution with a request ID and single-flight logic |
// Broken: duplicate execution path
import { AgentExecutor } from "langchain/agents";
async function handleChatMessage(input: string) {
const result = await agentExecutor.invoke({
input,
});
return result;
}
// Somewhere else in the app:
button.addEventListener("click", async () => {
// First call
await handleChatMessage("Book me a flight");
// Second accidental call from another listener / rerender / retry
await handleChatMessage("Book me a flight");
});
The fix is to make the agent run idempotent per request:
// Fixed: single-flight per requestId
import { AgentExecutor } from "langchain/agents";
const inFlight = new Map<string, Promise<unknown>>();
async function handleChatMessage(input: string, requestId: string) {
if (inFlight.has(requestId)) {
return inFlight.get(requestId);
}
const run = agentExecutor.invoke({ input }).finally(() => {
inFlight.delete(requestId);
});
inFlight.set(requestId, run);
return run;
}
If you are using LangChain’s tool-calling agents, this matters even more with classes like AgentExecutor, RunnableSequence, ChatOpenAI, and tool wrappers created via DynamicStructuredTool or tool(). One duplicated invocation can produce repeated tool calls like:
- •
Error: Tool call already exists - •
duplicate tool calls - •repeated
tool_callsentries in the model output
Other Possible Causes
1) You are retrying non-idempotent agent runs
If you wrap the entire agent call in a retry library, one timeout can replay the whole chain and execute tools again.
// Bad: retries the full side-effecting workflow
import pRetry from "p-retry";
await pRetry(() => agentExecutor.invoke({ input }), {
retries: 3,
});
Fix it by retrying only safe network reads, or by making tools idempotent:
// Better: idempotent tool execution keyed by operationId
await agentExecutor.invoke({
input,
metadata: { operationId },
});
2) Your stream handler processes the same chunk twice
This happens when you subscribe to both raw token events and final message events, then dispatch tool execution from each path.
// Bad: two handlers can trigger the same downstream action
llm.on("message", onMessage);
llm.on("data", onMessage);
Keep one source of truth for tool execution. If you are using streamEvents() or streamLog(), only consume one event type for orchestration.
3) You are mixing manual tool execution with agent-managed tools
If LangChain is already handling tool calls through an agent, do not also call the same tool directly in your app logic.
// Bad
const aiResult = await agentExecutor.invoke({ input });
await sendEmailTool.invoke({ to: "a@b.com", subject: "..." }); // duplicate side effect
Let either:
- •the agent own tool execution, or
- •your app own it after parsing structured output
Do not do both.
4) The same user message is submitted twice by your frontend
React strict mode, double-submit bugs, and optimistic UI code often cause two identical requests.
// Bad: no submit guard
form.addEventListener("submit", async (e) => {
e.preventDefault();
await fetch("/api/chat", { method: "POST", body: JSON.stringify({ input }) });
});
Add a submit lock and request ID:
let submitting = false;
form.addEventListener("submit", async (e) => {
e.preventDefault();
if (submitting) return;
submitting = true;
try {
await fetch("/api/chat", {
method: "POST",
headers: { "x-request-id": crypto.randomUUID() },
body: JSON.stringify({ input }),
});
} finally {
submitting = false;
}
});
How to Debug It
- •
Log every agent entry point
- •Add logs before every
agentExecutor.invoke(),stream(), and route handler. - •Include
requestId, user ID, and timestamp. - •If you see two identical logs for one click, your bug is outside LangChain.
- •Add logs before every
- •
Trace tool execution separately
- •Log inside each tool function.
- •If the agent runs once but the tool logs twice, your issue is in streaming/event handling.
- •If both log twice, your app is invoking the whole chain twice.
- •
Disable retries temporarily
- •Remove
pRetry, HTTP client retries, queue re-delivery, and background job retries. - •If duplicates disappear, your retry policy is replaying side effects.
- •Remove
- •
Inspect request boundaries
- •Check whether your frontend sends duplicate POSTs.
- •Look at React Strict Mode, debounced handlers, websocket reconnects, and serverless replays.
- •In production systems, this is often where “duplicate tool calls” starts.
Prevention
- •Make every agent run idempotent with a unique
requestIdoroperationId. - •Keep side effects inside one layer only: either LangChain tools or application code.
- •Add structured logging around:
- •route entry
- •agent invocation
- •tool execution
- •response completion
If you are building production agents with LangChain TypeScript, treat tool calls like database writes. Once they become side effects, duplicates are a control-plane problem — not an LLM problem.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
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