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

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

If you see state not updating when scaling in a LangChain TypeScript app, you’re usually dealing with a graph or agent state issue, not an LLM issue. The failure tends to show up when you move from a single-node flow to a parallel or multi-step workflow and one branch is writing to state in a way LangChain can’t merge.

In practice, this happens most often with StateGraph, reducers, or mutable state objects passed through nodes. The symptom is simple: the node runs, but downstream steps keep seeing stale state.

The Most Common Cause

The #1 cause is mutating shared state instead of returning a new partial update that LangChain can merge.

With StateGraph in LangChain JS/TS, each node should return a patch, not directly mutate the input object. If you push into an array or modify nested fields in place, the graph may not detect the change correctly during fan-out/fan-in execution.

Broken vs fixed

Broken patternRight pattern
Mutates state.messages in placeReturns a new array/object
Relies on shared object identityUses immutable updates
Fails under parallel branchesMerges cleanly through reducers
import { StateGraph, Annotation } from "@langchain/langgraph";

const GraphState = Annotation.Root({
  messages: Annotation<string[]>({
    reducer: (left, right) => [...left, ...right],
    default: () => [],
  }),
});

const badNode = async (state: typeof GraphState.State) => {
  state.messages.push("new message"); // mutation
  return state; // same object reference
};

const goodNode = async (state: typeof GraphState.State) => {
  return {
    messages: ["new message"], // patch only
  };
};

The broken version often “works” in a simple single-step test and then fails once you add branching. That’s because LangGraph expects deterministic state merges, and mutation breaks that contract.

Other Possible Causes

1) Missing reducer for concurrent writes

If two nodes write to the same key at the same time and that key has no reducer, one update can overwrite the other or trigger inconsistent behavior.

const State = Annotation.Root({
  logs: Annotation<string[]>({
    reducer: (left, right) => [...left, ...right],
    default: () => [],
  }),
});

Without the reducer, concurrent updates to logs are not safely combined.

2) Returning the wrong shape from a node

A node must return only the fields it updates. Returning nested objects in an unexpected shape can make it look like state is not changing.

// Wrong
return {
  state: {
    messages: ["hello"],
  },
};

// Right
return {
  messages: ["hello"],
};

This comes up when developers wrap everything under state out of habit from Redux-style code.

3) Reusing stale closures inside async nodes

If your node captures old values before an await, then writes based on stale data afterward, your update may appear lost.

const node = async (state: { count: number }) => {
  const current = state.count;
  await someAsyncCall();
  return { count: current + 1 }; // stale if another branch updated count
};

Prefer reducers or derive updates from local event data rather than assuming the input state is still current after async work.

4) Mixing mutable external stores with graph state

If one part of your app writes to Redis/Postgres/in-memory cache while LangGraph reads from its own internal state snapshot, they will drift apart.

// Bad idea if this is meant to be source of truth for graph execution
cache.set("conversation", updatedMessages);
return { messages: updatedMessages };

Keep one source of truth per execution path. If you need persistence, checkpoint LangGraph state explicitly instead of syncing ad hoc copies.

How to Debug It

  1. Print the exact node output

    • Add logging before every return.
    • Confirm the node returns { fieldName: value }, not the full mutated object.
  2. Check whether the field has a reducer

    • Any field written by more than one node needs a reducer.
    • In Annotation.Root, verify arrays and counters are merged intentionally.
  3. Test with one branch only

    • Temporarily remove parallel edges.
    • If the bug disappears, you likely have concurrent writes without proper merging.
  4. Inspect object identity

    • If you mutate arrays or nested objects in place, clone them first.
    • A quick check is comparing references before and after your update logic.
const before = state.messages;
const next = [...before, "hello"];

console.log(before === next); // false — good

If you’re using Runnable chains instead of graphs, apply the same rule: don’t mutate shared inputs between steps unless you fully control execution order.

Prevention

  • Treat LangChain/LangGraph state as immutable patches.
  • Add reducers for any field that can be written by more than one node.
  • Keep node outputs small and explicit; return only changed keys.
  • Write one test for linear flow and one for parallel fan-out/fan-in before shipping.

If you’re still seeing state not updating when scaling, start by searching for in-place mutation and missing reducers. In TypeScript LangChain apps, those two issues explain most “it works locally but breaks when I add more branches” failures.


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