How to Fix 'state not updating in production' in LangGraph (Python)

By Cyprian AaronsUpdated 2026-04-21
state-not-updating-in-productionlanggraphpython

Opening

If your LangGraph app works locally but stops updating state in production, the problem is usually not LangGraph itself. It means your graph is writing to a state object that is either not persisted, not merged correctly, or being overwritten by a bad reducer or concurrency bug.

This usually shows up after deployment when you move from a single-process dev setup to multiple workers, async execution, or a real checkpointer like Postgres or Redis.

The Most Common Cause

The #1 cause is mutating state in place instead of returning a new partial update from the node.

LangGraph expects nodes to return a dict of updates keyed by state fields. If you mutate the existing state object and return nothing, the graph has nothing to persist.

Broken vs fixed pattern

Broken patternFixed pattern
Mutates state in placeReturns a partial update dict
Works “sometimes” in local testsFails in production because updates are not checkpointed
Hard to debug because no exception is raisedDeterministic and checkpoint-friendly
# BROKEN
from typing import TypedDict
from langgraph.graph import StateGraph, END

class State(TypedDict):
    messages: list[str]
    counter: int

def increment_counter(state: State):
    state["counter"] += 1   # in-place mutation
    # returns None -> LangGraph has no update to persist

graph = StateGraph(State)
graph.add_node("increment_counter", increment_counter)
graph.set_entry_point("increment_counter")
graph.add_edge("increment_counter", END)
app = graph.compile()
# FIXED
from typing import TypedDict
from langgraph.graph import StateGraph, END

class State(TypedDict):
    messages: list[str]
    counter: int

def increment_counter(state: State):
    return {"counter": state["counter"] + 1}

graph = StateGraph(State)
graph.add_node("increment_counter", increment_counter)
graph.set_entry_point("increment_counter")
graph.add_edge("increment_counter", END)
app = graph.compile()

If you need to append to a list, return the new value or use a reducer designed for accumulation.

from typing_extensions import Annotated
from operator import add
from typing import TypedDict

class State(TypedDict):
    messages: Annotated[list[str], add]

That reducer tells LangGraph how to merge concurrent updates instead of replacing the field blindly.

Other Possible Causes

1) No checkpointer in production

A very common production bug is compiling the graph without persistence. In-memory state disappears between requests, worker restarts, and replica hops.

# BAD: no persistence
app = graph.compile()
# GOOD: attach a checkpointer
from langgraph.checkpoint.memory import MemorySaver

checkpointer = MemorySaver()
app = graph.compile(checkpointer=checkpointer)

For real production traffic, use a durable backend like Postgres. MemorySaver is only for local testing.

2) Missing thread_id in config

LangGraph uses the thread ID to identify which conversation/state bucket to load and save. If you omit it, every request can look like a fresh run.

# BAD
result = app.invoke({"messages": ["hi"]})
# GOOD
result = app.invoke(
    {"messages": ["hi"]},
    config={"configurable": {"thread_id": "user-123"}}
)

If you are using RunnableConfig, make sure the configurable.thread_id value is stable per user/session.

3) Wrong state schema or missing reducers

If two nodes write to the same field and your schema does not define how to merge them, one update can overwrite the other. This often looks like “state not updating” when it is actually being replaced.

from typing import TypedDict

class State(TypedDict):
    messages: list[str]   # no reducer defined

Fix it with an annotated reducer:

from typing_extensions import Annotated
from operator import add
from typing import TypedDict

class State(TypedDict):
    messages: Annotated[list[str], add]

Use reducers for append-only fields like messages, events, logs, or tool outputs.

4) Returning the wrong shape from the node

LangGraph nodes must return a mapping of updated fields. Returning a raw string, tuple, or nested object that does not match your schema will not update state correctly.

# BAD
def node(state):
    return "done"
# GOOD
def node(state):
    return {"status": "done"}

If you see errors like:

  • InvalidUpdateError
  • Expected dict, got ...
  • State update must be a mapping

then this is probably your issue.

How to Debug It

  1. Print every node input and output

    • Add logging inside each node.
    • Confirm the function returns a dict with keys that exist in your state schema.
  2. Check whether you are using persistence

    • Verify compile(checkpointer=...).
    • If you are on multiple workers, confirm the same backend is used across all replicas.
  3. Verify your thread identity

    • Log config["configurable"]["thread_id"].
    • Make sure it stays constant across requests for the same conversation.
  4. Inspect reducers and concurrent writes

    • Look for fields updated by more than one node.
    • If two nodes write to messages, events, or similar lists, add an explicit reducer like operator.add.

A quick test:

result = app.invoke(
    {"messages": ["hello"], "counter": 0},
    config={"configurable": {"thread_id": "debug-1"}}
)
print(result)

If this works once but not across calls with the same thread ID, your issue is almost always checkpointing or config identity.

Prevention

  • Always treat LangGraph state as immutable input/output data.
    • Return partial updates; do not mutate in place.
  • Use a durable checkpointer in production.
    • MemorySaver is fine for local dev only.
  • Define reducers for append-only fields.
    • Especially for message histories and event logs.
  • Standardize thread IDs early.
    • Tie them to user/session IDs and log them on every request.

If you build graphs this way from day one, “state not updating in production” stops being a mystery and becomes a quick config check.


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