How to Fix 'state not updating when scaling' in LangGraph (TypeScript)
Opening
state not updating when scaling in LangGraph usually means your graph works for one input, then starts returning stale or partial state once you add parallel branches, retries, or multiple workers. In TypeScript, this almost always comes from a bad reducer, mutating state in place, or using a state shape that can’t merge concurrent updates.
The symptom is consistent: nodes run, logs look fine, but the final StateGraph output never reflects the latest values. If you’re seeing this after moving from a single-threaded local run to a scaled setup, you’re in the right place.
The Most Common Cause
The #1 cause is an invalid state update pattern: returning mutated objects instead of immutable partial updates, or defining a field without a reducer when multiple nodes write to it.
LangGraph merges node outputs through the graph state schema. If two nodes update the same key and that key has no reducer, the last write wins. In parallel execution, that often looks like “state not updating” because one branch overwrites another.
Broken vs fixed
| Broken pattern | Fixed pattern |
|---|---|
| Mutates existing state and returns it | Returns a new partial object |
| No reducer for concurrently written field | Uses Annotation reducer |
| Assumes object spread is enough under concurrency | Makes merge semantics explicit |
import { Annotation, StateGraph, START, END } from "@langchain/langgraph";
type MyState = {
messages: string[];
counter: number;
};
// ❌ Broken: no reducer for messages
const State = Annotation.Root({
messages: Annotation<string[]>(),
counter: Annotation<number>(),
});
const graph = new StateGraph(State)
.addNode("a", async (state) => {
state.messages.push("from a"); // mutation
return { messages: state.messages };
})
.addNode("b", async (state) => {
state.messages.push("from b"); // mutation
return { messages: state.messages };
})
.addEdge(START, "a")
.addEdge(START, "b")
.addEdge("a", END)
.addEdge("b", END);
import { Annotation, StateGraph, START, END } from "@langchain/langgraph";
// ✅ Fixed: explicit reducer for concurrent writes
const State = Annotation.Root({
messages: Annotation<string[]>({
reducer: (left = [], right = []) => [...left, ...right],
default: () => [],
}),
counter: Annotation<number>({
reducer: (_left = 0, right = 0) => right,
default: () => 0,
}),
});
const graph = new StateGraph(State)
.addNode("a", async () => {
return { messages: ["from a"] };
})
.addNode("b", async () => {
return { messages: ["from b"] };
})
.addEdge(START, "a")
.addEdge(START, "b")
.addEdge("a", END)
.addEdge("b", END);
If you’re updating arrays, objects, or counters from more than one node in the same superstep, define the reducer. Without it, LangGraph is doing exactly what you asked — just not what you meant.
Other Possible Causes
1. You’re returning a full state object instead of a patch
Nodes should usually return only the keys they changed. Returning a whole copied object can reintroduce stale values from earlier snapshots.
// ❌ Bad
return { ...state, status: "done" };
// ✅ Good
return { status: "done" };
2. Your field type doesn’t match the reducer shape
If your reducer expects arrays but your node returns a scalar, merging gets weird fast.
// ❌ Bad
const State = Annotation.Root({
tags: Annotation<string[]>({
reducer: (l = [], r = []) => [...l, ...r],
default: () => [],
}),
});
return { tags: "urgent" };
// ✅ Good
return { tags: ["urgent"] };
3. You have hidden mutation inside nested objects
This is common with deeply nested state and shared references.
// ❌ Bad
const next = state.profile;
next.name = "Ada";
return { profile: next };
// ✅ Good
return {
profile: {
...state.profile,
name: "Ada",
},
};
4. Parallel branches are writing the same key without coordination
If two nodes update result, one may overwrite the other unless you define merge behavior.
const State = Annotation.Root({
result: Annotation<string>({
// last write wins; okay only if that's intended
reducer: (_l = "", r = "") => r,
default: () => "",
}),
});
If you actually need both values:
const State = Annotation.Root({
resultParts: Annotation<string[]>({
reducer: (l = [], r = []) => [...l, ...r],
default: () => [],
}),
});
How to Debug It
- •
Check whether the field is updated by multiple nodes
- •Search for every
return { yourField }. - •If more than one node writes to it in the same branch level, you need a reducer.
- •Search for every
- •
Log before and after each node
- •Print only the relevant slice of state.
- •Look for mutations that happen before
return.
.addNode("step", async (state) => {
console.log("before", JSON.stringify(state));
const nextMessages = [...state.messages, "new"];
console.log("after", JSON.stringify(nextMessages));
return { messages: nextMessages };
})
- •
Temporarily force sequential execution
- •Remove parallel edges.
- •If the bug disappears, it’s almost certainly a merge/reducer issue.
- •
Verify your annotation schema
- •Make sure every concurrently written field has an explicit
reducer. - •Check defaults too; missing defaults can produce confusing runtime behavior.
- •Make sure every concurrently written field has an explicit
Prevention
- •Use immutable updates only.
- •Add reducers for any field that can be written by more than one node.
- •Keep node outputs as small patches instead of full-state copies.
- •Test graphs with both single-path and parallel-path execution before shipping.
If you want stable behavior under scale-out conditions, treat LangGraph state like distributed data merging logic — because that’s what it is. The bug usually isn’t LangGraph “not updating”; it’s your schema telling LangGraph to overwrite instead of merge.
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