How to Fix 'state not updating when scaling' in LangGraph (TypeScript)

By Cyprian AaronsUpdated 2026-04-21
state-not-updating-when-scalinglanggraphtypescript

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 patternFixed pattern
Mutates existing state and returns itReturns a new partial object
No reducer for concurrently written fieldUses Annotation reducer
Assumes object spread is enough under concurrencyMakes 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

  1. 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.
  2. 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 };
})
  1. Temporarily force sequential execution

    • Remove parallel edges.
    • If the bug disappears, it’s almost certainly a merge/reducer issue.
  2. Verify your annotation schema

    • Make sure every concurrently written field has an explicit reducer.
    • Check defaults too; missing defaults can produce confusing runtime behavior.

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

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