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 vSection 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 ENDTo 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_nameEvery 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 target | Best for | Latency | Concurrency | Cost profile |
|---|---|---|---|---|
| Docker (single host) | Internal tools, low volume | Low | Limited by host | Predictable |
| Kubernetes | Bursty workloads, parallel agents | Low to medium | Horizontal scale | Variable |
| Cloud Run / Lambda | Async, event driven, low frequency | Medium (cold start) | Automatic | Pay-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.