Agentic AIAI Engineering10 min readUpdated

LangGraph in Production: Patterns and Pitfalls

By Mudassir Khan — Agentic AI Consultant & AI Systems Architect, Islamabad, Pakistan

Cover illustration for: LangGraph in Production: Patterns and Pitfalls

Quick answer

Is LangGraph production ready? Yes — with caveats. LangGraph has documented production deployments at Uber, LinkedIn, and JP Morgan. What it does not handle for you: state durability, retry design, and observability. Configure a persistent checkpointer, set explicit retry budgets, and wire LangSmith tracing. Get those three right and LangGraph is a solid foundation for production agentic systems.

Section 01 · Framing

What breaks in production that works in tutorials

LangGraph tutorials make four assumptions production systems cannot afford: stateless execution, no retries, no observability, and no cost control.

LangGraph tutorials are written to teach the framework, not to deploy it. They run graphs end to end in a single process against a crafted test prompt. Production systems run inside web servers, queue consumers, or serverless functions — processes that restart, deployments that roll, pods that get evicted. Four assumptions tutorials make that production immediately violates:

Stateless execution

Tutorials hold all state in process memory. In production, that memory disappears on restart. Any in progress run that was not checkpointed to a durable backend is gone.

No retries

Tutorials call the LLM API once per node. In production, the API returns rate limits, transient 500s, and timeouts. Tool calls hit external services that go down. Without retry logic, a single transient failure aborts the entire run.

No observability

You cannot debug a graph you cannot see. Printing intermediate state to stdout is not enough in production. You need structured traces: node latency, token counts, tool call arguments and results.

No cost control

Tutorials run a handful of steps. Production workloads run thousands of parallel agents against edge case inputs. An unbounded retry loop or uncapped message history can burn hundreds of dollars before anyone notices.

These are not edge cases. They are the four walls of production reality. The sections below address each one directly with patterns you can implement today.

Section 02 · Durability

Checkpointing state durably

LangGraph's checkpoint system saves a snapshot of graph state after every node. The built-in MemorySaver is a tutorial convenience. Never use it in production.

When you compile a graph with a checkpointer, each step is persisted to the configured backend under a thread ID. If the process crashes mid run, passing the same thread ID to graph.invoke resumes from the last saved node — no replay of completed work required.

The three checkpoint backends for production:

PostgresSaver

The right default for most teams. Durable, transactional, and works with any managed Postgres instance (RDS, Cloud SQL, Supabase). Higher per checkpoint latency than Redis, which only matters for graphs with many very short nodes.

RedisSaver

Best when checkpoint latency is on the critical path — live user facing agents with tight response time budgets. Configure AOF persistence or Redis Cluster with replicas to avoid losing state on Redis restart.

SQLiteSaver

Fine for local development. Does not handle concurrent writers. Do not use it in production.

from langgraph.checkpoint.postgres import PostgresSaver
import psycopg

DB_URI = "postgresql://user:password@host:5432/agents"

with psycopg.connect(DB_URI, autocommit=True) as conn:
    checkpointer = PostgresSaver(conn)
    checkpointer.setup()  # creates checkpoint tables on first run

graph = workflow.compile(checkpointer=checkpointer)

# each invocation gets a thread_id; same thread_id resumes from last checkpoint
config = {"configurable": {"thread_id": "run-abc123"}}
result = graph.invoke({"messages": [...]}, config=config)

Store the thread_id with the job record in your database. On restart, pass the same ID and LangGraph skips already-completed nodes.

Define your state schema with Pydantic and enforce a maximum message history length in the validator. Unbounded message history is the direct cause of the state explosion failure mode described later. Add this to every production graph:

from pydantic import BaseModel, field_validator
from typing import Annotated
from langgraph.graph.message import add_messages

class AgentState(BaseModel):
    messages: Annotated[list, add_messages]

    @field_validator("messages")
    @classmethod
    def cap_message_history(cls, v):
        MAX_MESSAGES = 40  # tune per your context budget
        if len(v) > MAX_MESSAGES:
            # keep the system message plus the most recent window
            return [v[0]] + v[-(MAX_MESSAGES - 1):]
        return v

Section 03 · Resilience

Handling partial failures and retries

Production LangGraph agents fail in three ways: LLM API errors, tool call failures, and node exceptions. Each needs a different response.

LLM API errors (rate limits, transient 500s, timeouts) call for exponential backoff with jitter and a hard retry cap. Tool call failures that return empty or malformed results should not be retried blindly — the agent needs to detect persistent tool failure and route to a fallback or surface the error. Node exceptions that propagate uncaught abort the run without checkpointing, losing all intermediate state.

Here is a retry-wrapped node with a hard cap and structured error handling:

import time
import random

def with_retry(fn, max_attempts: int = 3, base_delay: float = 1.0):
    """Wrap a node function with exponential backoff retry."""
    def wrapper(state):
        last_error = None
        for attempt in range(max_attempts):
            try:
                return fn(state)
            except Exception as e:
                last_error = e
                if attempt < max_attempts - 1:
                    delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
                    time.sleep(delay)
        # return structured error state instead of raising
        return {
            "error": f"Node failed after {max_attempts} attempts: {str(last_error)}",
            "failed": True,
        }
    return wrapper

@with_retry
def call_llm_node(state):
    response = llm.invoke(state["messages"])
    return {"messages": [response]}

The conditional routing function must check for the error state explicitly. A common mistake is writing a should_continue function that only checks for tool calls in the last message, without a branch for the exhausted-retry case:

def should_continue(state) -> str:
    if state.get("failed"):
        return "error_handler"
    if state.get("cost_exceeded"):
        return "error_handler"
    last_message = state["messages"][-1]
    if last_message.tool_calls:
        return "tools"
    return END

To resume after a partial failure, pass the original thread_id to the next invocation. LangGraph replays from the last successful checkpoint, skipping nodes that already completed.

Section 04 · Concurrency

Managing concurrency

LangGraph supports parallel node execution via fan out and fan in. Three things to get right: reducer annotations, scope discipline, and subgraph encapsulation.

Parallel edges in LangGraph run multiple nodes simultaneously and merge their outputs into a downstream node. This is the right pattern when subtasks are genuinely independent — calling multiple tools in parallel, processing multiple documents simultaneously. Do not reach for parallel edges to speed up a sequential task; graph overhead negates the gain.

The critical detail: if two parallel nodes both write to the same state key, the last writer wins and you silently lose data. Fix this with a reducer annotation — a function that merges parallel writes rather than overwriting. LangGraph’s built-in add_messages is one example. For custom fields, use Python’s operator.add or a custom merge function:

from langgraph.graph import StateGraph, START, END
from operator import add
from typing import Annotated, TypedDict

class ParallelState(TypedDict):
    # add as reducer: parallel writes append rather than overwrite
    results: Annotated[list[str], add]

def fetch_source_a(state: ParallelState) -> dict:
    return {"results": ["result from source A"]}

def fetch_source_b(state: ParallelState) -> dict:
    return {"results": ["result from source B"]}

def synthesize(state: ParallelState) -> dict:
    combined = "
".join(state["results"])
    return {"synthesis": combined}

builder = StateGraph(ParallelState)
builder.add_node("fetch_a", fetch_source_a)
builder.add_node("fetch_b", fetch_source_b)
builder.add_node("synthesize", synthesize)

builder.add_edge(START, "fetch_a")
builder.add_edge(START, "fetch_b")
builder.add_edge("fetch_a", "synthesize")
builder.add_edge("fetch_b", "synthesize")
builder.add_edge("synthesize", END)

When a parallel subtask is complex enough to have its own state schema and conditional routing, encapsulate it as a subgraph rather than expanding it inline. Subgraphs have isolated state namespaces, which eliminates the parallel write race condition entirely for that subtask's internal state.

Section 05 · Observability

Observability with LangSmith

Wiring LangSmith tracing takes one environment variable. Skipping it means debugging production failures blind.

LANGCHAIN_TRACING_V2=true
LANGCHAIN_API_KEY=your_api_key
LANGCHAIN_PROJECT=your_project_name

Every graph invocation is automatically traced: node entry and exit, inputs and outputs, LLM calls with token counts, tool calls with arguments and return values. No code changes required beyond the environment variables.

Three metrics predict production failure before it happens:

Node latency by percentile

A node that averages 2 seconds but has a p99 of 45 seconds has a timeout problem that average latency hides. Track p50, p95, and p99 per node. Any node with a p99 more than 5x its p50 has a reliability issue worth investigating before it becomes an incident.

Token count per node, per run — as a trend

Steadily increasing token counts across runs with the same task type indicate state explosion in progress. The agent's message history is growing run over run. Track this as a trend, not just a per-run snapshot.

Tool call success rate

A tool that succeeds 95% of the time in development succeeds 95% of the time in production too — which means it fails on 1 in 20 calls. At scale, that rate produces visible failure patterns. Alert when any tool success rate drops below 90%.

Section 06 · Deployment

Deployment patterns

Three viable deployment targets for production LangGraph agents, each with a different trade off profile.

Deployment targetBest forLatencyConcurrencyCost profile
Docker (single host)Internal tools, low volumeLowLimited by hostPredictable
KubernetesBursty workloads, parallel agentsLow to mediumHorizontal scaleVariable
Cloud Run / LambdaAsync, event driven, low frequencyMedium (cold start)AutomaticPay-per-use

Docker on a single host is the fastest path to production for low-volume internal tooling. The LangGraph CLI (langgraph up) builds and runs a Docker image from your project directory with production settings. The limit is the host: one process pool means memory-bound graphs with large state objects cap out quickly.

Kubernetes suits teams running parallel agents — many concurrent graph invocations serving user traffic or processing job queues. Deploy the LangGraph server as a Kubernetes Deployment with horizontal pod autoscaling based on queue depth or request rate. Keep the checkpoint backend (Postgres) outside the pod cluster in a managed service so state survives pod evictions. Configure readiness probes to prevent traffic routing to pods before they are warmed up.

Serverless (Cloud Run, AWS Lambda) suits event driven or batch workflows where invocations are infrequent and async. The main trade off is cold start latency: a Python LangGraph container cold starts in 2 to 8 seconds depending on dependency weight. Use minimum instances or provisioned concurrency for latency-sensitive paths.

One rule that applies to all three targets: the LangGraph graph process should be stateless. All state lives in the checkpoint backend. This makes the process horizontally scalable — any instance can pick up any run by thread ID. Architect for this from day one; retrofitting stateful graph code to a distributed checkpoint model is painful.

Section 07 · Failure Modes

The three failure modes that kill LangGraph agents at scale

These are not theoretical. Each has caused production incidents at organizations running LangGraph at scale.

State explosion

The agent's message history grows without bound. Each turn appends user message, assistant response, and tool call results. After 30 to 50 turns, the total token count exceeds the model's context window and the LLM API returns a context length error — at step 47, after 46 steps have already executed. Fix: enforce a rolling window cap in your Pydantic state validator. Add a summary node that compresses history when the message count approaches the cap for long-running tasks.

Tool retry storms

A tool call fails with a transient error. The retry wrapper retries. The error persists. The graph loops back through the tool-calling node without a hard cap. One startup documented 200 LLM calls in 10 minutes burning $200 before anyone noticed. Fix: every retry wrapper needs a maximum attempt count. The graph's conditional routing needs an explicit branch for the retry-exhausted state that routes to an error handler rather than looping.

Checkpoint drift

A graph processes 40 nodes of a 50-node run and checkpoints state along the way. A new deployment changes the graph definition: a node is renamed, a state field is added. The old checkpoint state is now incompatible with the new graph. On resume, the agent either raises a deserialization error or routes to the wrong node silently. Fix: version your graph definitions. Tag checkpoints with the graph version that created them. On resume, compare checkpoint version to deployed version — for breaking changes, surface the incompatibility rather than attempting a resume.

Section 08 · Checklist

Production readiness checklist

Run through this before shipping any LangGraph agent to production.

Checkpointing

PostgresSaver or RedisSaver configured; MemorySaver removed from production code

State schema

Pydantic model with explicit message history cap enforced in field_validator

Retry logic

Hard maximum on retry attempts per node; structured error state returned on exhaustion

Cost guard

Per-run token budget check in conditional routing with abort path to error handler

Graph versioning

Checkpoint schema tagged with graph version; resume guards check for version mismatch

Concurrency

Reducer annotations on every state field written by parallel nodes

Observability

LangSmith tracing enabled; node latency p99, token count trend, and tool success rate alerts configured

Stateless process

Graph process holds no state; all state lives in external checkpoint backend

Error handler node

Explicit graph node for failed, cost-exceeded, and version-mismatch states

Load test

50 to 100 concurrent graph invocations run to verify checkpoint backend throughput holds under load

FAQ

Frequently asked questions

The questions engineering teams ask most when moving LangGraph from prototype to production.

How do you deploy LangGraph?

Three practical options: Docker via the langgraph up CLI command for low-volume internal tooling, Kubernetes with horizontal pod autoscaling and an external Postgres checkpoint backend for bursty parallel workloads, and serverless on Cloud Run or AWS Lambda for async event driven workflows tolerant of cold start latency. In all cases, the graph process should be stateless — all state lives in the checkpoint store so any instance can resume any run by thread ID.

Is LangGraph production ready?

Yes, with caveats. LangGraph has documented production deployments at Uber, LinkedIn, and JP Morgan. The framework handles graph execution, conditional routing, and parallel node coordination correctly. What it does not handle for you: state durability (configure PostgresSaver or RedisSaver), retry design (you own the retry wrapper and budget cap), and observability (wire LangSmith tracing). Get those three right and LangGraph is a stable foundation.

LangGraph vs LangChain for production?

They solve different problems. LangChain is a library of LLM primitives: chains, prompt templates, document loaders. LangGraph is an orchestration layer for stateful, graph-structured agents with conditional routing and parallel execution. For production agentic systems that need durable state and complex routing logic, LangGraph is the right layer. LangChain components are typically used inside LangGraph nodes.

What is LangGraph checkpointing?

Checkpointing is LangGraph's persistence layer. When you compile a graph with a checkpointer, the full agent state is saved after every node execution, indexed by thread ID. On failure or restart, passing the same thread ID to graph.invoke resumes from the last saved checkpoint rather than starting over. PostgresSaver is the right backend for most production deployments.

How do you handle LangGraph agent failures in production?

Wrap all node functions with retry logic that has a hard cap and returns a structured error state on exhaustion. Add an error handler node to the graph and route all failure states to it. The error handler persists the failure to the checkpoint, logs structured error data to your observability stack, and returns a clean error response to the caller. Never let uncaught exceptions propagate out of nodes — they abort the run without checkpointing, losing all intermediate state.

How do I prevent LangGraph agents from running up large API bills?

Three controls: enforce a message history cap in your Pydantic state schema to prevent state explosion, add a per-run token budget check in the conditional routing function and route to an error handler when exceeded, and set hard maximum retry counts on all node wrappers. Configure LangSmith cost alerts as an out-of-band detection layer so you catch runaway behavior before it scales.

If you are building or migrating a production LangGraph system and need help with state design, checkpoint infrastructure, or the observability stack, the agentic AI consulting service covers all of this as part of its production readiness work. You can also read the comparison of LangGraph vs AutoGen vs CrewAI if you are still evaluating frameworks, or the AI systems architect role breakdown for the organizational context around this work.

Written by Mudassir Khan

Agentic AI consultant and AI systems architect based in Islamabad, Pakistan. CEO of Cube A Cloud. 38+ agentic AI launches delivered for global founders and CTOs.

View agentic AI consulting service

Related service

Agentic AI Consulting

See scope & pricing →

More on this topic

Need an AI systems architect?

Book a 30-minute architecture call. I will sketch the high-level design for your use case and give you an honest view of the trade-offs.

Book a strategy call →