Skip to content
Blog

LangGraph for Production: State Machines, Checkpointing, and Subgraphs

Take LangGraph to production with state machines, checkpointing, and subgraphs. Build reliable, observable, and scalable agent workflows.

Published on September 8, 2026

AI Assistant

LangGraph in a notebook is a prototype. LangGraph in production is a different beast entirely. You need state machines that handle failures gracefully, checkpointing that survives crashes, and subgraphs that keep complex workflows manageable. This guide covers the patterns that separate demo-quality LangGraph apps from production systems.

Production Requirements

Before deploying LangGraph, you need:

  • Deterministic state transitions — No ambiguous routing
  • Persistent state — Resume after crashes
  • Error recovery — Graceful handling of failures
  • Observability — Trace every decision
  • Scalability — Handle concurrent workflows

State Machines with TypedDict

Define strict state schemas:

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

class OrderState(TypedState):
    order_id: str
    customer_id: str
    items: list[dict]
    total: float
    status: Literal[
        "validating",
        "checking_inventory",
        "processing_payment",
        "fulfilling",
        "completed",
        "failed"
    ]
    errors: Annotated[list[str], add]
    retry_count: int

def validate_order(state: OrderState) -> OrderState:
    """Validate the order before processing."""
    errors = []
    
    if not state["items"]:
        errors.append("Order has no items")
    
    if state["total"] <= 0:
        errors.append("Invalid order total")
    
    if errors:
        return {"status": "failed", "errors": errors}
    
    return {"status": "checking_inventory"}

def check_inventory(state: OrderState) -> OrderState:
    """Check if all items are in stock."""
    out_of_stock = []
    
    for item in state["items"]:
        if not is_in_stock(item["sku"], item["quantity"]):
            out_of_stock.append(item["name"])
    
    if out_of_stock:
        return {
            "status": "failed",
            "errors": [f"Out of stock: {', '.join(out_of_stock)}"]
        }
    
    return {"status": "processing_payment"}

Durable Execution

LangGraph’s checkpointing provides durable execution:

from langgraph.checkpoint.postgres import PostgresSaver

# Production checkpointing with PostgreSQL
checkpointer = PostgresSaver.from_conn_string(
    "postgresql://user:pass@localhost:5432/agent_checkpoints"
)

graph = StateGraph(OrderState)
# ... add nodes and edges ...
app = graph.compile(checkpointer=checkpointer)

# Every execution is automatically checkpointed
config = {"configurable": {"thread_id": "order-123"}}

# If this crashes, resume with:
# app.invoke(None, config)  # Continues from last checkpoint

Subgraph Patterns

Break complex workflows into composable subgraphs:

# Payment subgraph
payment_graph = StateGraph(PaymentState)
payment_graph.add_node("validate_card", validate_card)
payment_graph.add_node("charge", charge_card)
payment_graph.add_node("handle_failure", handle_payment_failure)
payment_graph.add_edge(START, "validate_card")
payment_graph.add_conditional_edges(
    "validate_card",
    lambda s: "charge" if s.get("card_valid") else "handle_failure"
)
payment_graph.add_edge("charge", END)
payment_graph.add_edge("handle_failure", END)
payment_subgraph = payment_graph.compile(checkpointer=checkpointer)

# Fulfillment subgraph
fulfillment_graph = StateGraph(FulfillmentState)
fulfillment_graph.add_node("reserve_inventory", reserve_inventory)
fulfillment_graph.add_node("create_shipment", create_shipment)
fulfillment_graph.add_node("track", track_shipment)
fulfillment_graph.add_edge(START, "reserve_inventory")
fulfillment_graph.add_edge("reserve_inventory", "create_shipment")
fulfillment_graph.add_edge("create_shipment", "track")
fulfillment_graph.add_edge("track", END)
fulfillment_subgraph = fulfillment_graph.compile(checkpointer=checkpointer)

# Main graph uses subgraphs
main_graph = StateGraph(OrderState)
main_graph.add_node("validate", validate_order)
main_graph.add_node("inventory", check_inventory)
main_graph.add_node("payment", payment_subgraph)
main_graph.add_node("fulfillment", fulfillment_subgraph)
main_graph.add_node("complete", complete_order)

main_graph.add_edge(START, "validate")
main_graph.add_conditional_edges(
    "validate",
    lambda s: "inventory" if s["status"] != "failed" else END
)
main_graph.add_conditional_edges(
    "inventory",
    lambda s: "payment" if s["status"] != "failed" else END
)
main_graph.add_edge("payment", "fulfillment")
main_graph.add_edge("fulfillment", "complete")
main_graph.add_edge("complete", END)

production_app = main_graph.compile(checkpointer=checkpointer)

Error Handling and Retries

from langgraph.types import RetryPolicy

def resilient_node(state: OrderState) -> OrderState:
    """Node with automatic retry on failure."""
    try:
        result = call_external_service(state)
        return {"status": result["next_status"]}
    except TemporaryError as e:
        # LangGraph will retry this node
        raise
    
# Add retry policy to nodes
graph.add_node("payment", payment_node, retry=RetryPolicy(max_attempts=3))

# Custom retry logic
def should_retry(state: OrderState) -> Literal["retry", "fail"]:
    if state["retry_count"] < 3:
        return "retry"
    return "fail"

Observability

Structured Logging

import logging
from opentelemetry import trace

tracer = trace.get_tracer("order-processing")

def traced_node(node_name: str):
    def decorator(func):
        def wrapper(state: OrderState) -> OrderState:
            with tracer.start_as_current_span(
                node_name,
                attributes={
                    "order.id": state.get("order_id"),
                    "order.status": state.get("status"),
                }
            ) as span:
                try:
                    result = func(state)
                    span.set_attribute("result.status", result.get("status", "unknown"))
                    return result
                except Exception as e:
                    span.set_status(trace.StatusCode.ERROR, str(e))
                    span.record_exception(e)
                    raise
        return wrapper
    return decorator

@traced_node("validate_order")
def validate_order(state: OrderState) -> OrderState:
    # ... validation logic ...
    pass

Metrics Collection

from prometheus_client import Counter, Histogram

order_counter = Counter("orders_total", "Total orders processed", ["status"])
processing_time = Histogram("order_processing_seconds", "Order processing time")

def metrics_node(state: OrderState) -> OrderState:
    """Collect metrics for monitoring."""
    order_counter.labels(status=state["status"]).inc()
    return state

Testing Production Workflows

import pytest

@pytest.fixture
def production_app():
    checkpointer = MemorySaver()  # Use memory for tests
    graph = build_order_graph(checkpointer)
    return graph.compile()

def test_happy_path(production_app):
    config = {"configurable": {"thread_id": "test-1"}}
    
    result = production_app.invoke({
        "order_id": "test-1",
        "items": [{"sku": "ABC", "quantity": 1}],
        "total": 29.99,
        "status": "validating",
        "errors": [],
        "retry_count": 0,
    }, config)
    
    assert result["status"] == "completed"

def test_crash_recovery(production_app):
    config = {"configurable": {"thread_id": "test-2"}}
    
    # Start processing
    production_app.invoke({"order_id": "test-2", ...}, config)
    
    # Simulate crash and resume
    result = production_app.invoke(None, config)
    
    # Should resume from checkpoint
    assert result["status"] in ["processing_payment", "completed"]

Deployment

Docker

FROM python:3.12-slim
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt

# Health check
HEALTHCHECK CMD curl -f http://localhost:8080/health || exit 1

EXPOSE 8080
CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8080"]

Kubernetes

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-agent
spec:
  replicas: 3
  selector:
    matchLabels:
      app: order-agent
  template:
    spec:
      containers:
      - name: order-agent
        image: order-agent:latest
        resources:
          limits:
            memory: "512Mi"
            cpu: "500m"
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: agent-secrets
              key: database-url

Conclusion

Production LangGraph requires more than just compiling a graph. You need typed state for predictability, checkpointing for durability, subgraphs for composability, error handling for resilience, and observability for debugging. Start with typed states and checkpointing, add subgraphs as complexity grows, and instrument everything from day one.