DEV Community

Phil IPS
Phil IPS

Posted on

Building Production AI Applications with LangGraph: State, Graphs & Deployment

Building Production AI Applications with LangGraph: State, Graphs & Deployment

Master LangGraph for building production AI applications — stateful graphs, persistence, streaming, human-in-the-loop, and deployment patterns.

Introduction

LangGraph transforms LangChain chains into stateful, cyclic graphs — enabling agents, multi-step workflows, and complex state management. This guide covers production patterns.

Why LangGraph?

LangChain (Chains) LangGraph (Graphs)
Linear, acyclic Cyclic, branching
Stateless Full state management
Single-threaded Concurrent execution
Hard to debug Visualizable, inspectable
Limited human-in-the-loop Built-in interruption

Part 1: Core Concepts

State & Graph Definition

# langgraph/core.py
from typing import TypedDict, Annotated, List, Optional
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langchain_core.messages import BaseMessage, HumanMessage, AIMessage

class AgentState(TypedDict):
    messages: Annotated[List[BaseMessage], add_messages]
    user_id: str
    session_id: str
    context: dict
    current_step: str
    retry_count: int
    metadata: dict

def create_basic_graph():
    workflow = StateGraph(AgentState)

    # Add nodes
    workflow.add_node("process_input", process_input_node)
    workflow.add_node("reason", reason_node)
    workflow.add_node("act", act_node)
    workflow.add_node("respond", respond_node)

    # Define edges
    workflow.add_edge(START, "process_input")
    workflow.add_edge("process_input", "reason")
    workflow.add_conditional_edges("reason", route_after_reason, {
        "act": "act",
        "respond": "respond"
    })
    workflow.add_edge("act", "reason")
    workflow.add_edge("respond", END)

    return workflow.compile()

async def process_input_node(state: AgentState):
    # Preprocess, validate, enrich
    return {"current_step": "input_processed"}

async def reason_node(state: AgentState):
    # LLM reasoning
    return {"current_step": "reasoned"}

def route_after_reason(state: AgentState) -> str:
    # Decide next action
    return "act" if needs_action(state) else "respond"

async def act_node(state: AgentState):
    # Tool execution
    return {"current_step": "acted"}

async def respond_node(state: AgentState):
    # Generate final response
    return {"current_step": "responded"}
Enter fullscreen mode Exit fullscreen mode

Checkpointers (Persistence)

# langgraph/persistence.py
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.checkpoint.postgres import PostgresSaver
from langgraph.checkpoint.redis import RedisSaver
import sqlite3

# SQLite (development)
sqlite_conn = sqlite3.connect("checkpoints.db", check_same_thread=False)
sqlite_checkpointer = SqliteSaver(sqlite_conn)

# PostgreSQL (production)
postgres_checkpointer = PostgresSaver.from_conn_string(
    "postgresql://user:pass@localhost/langgraph"
)

# Redis (high-throughput)
redis_checkpointer = RedisSaver.from_conn_string(
    "redis://localhost:6379"
)

# Usage with checkpointer
app = workflow.compile(checkpointer=sqlite_checkpointer)

# Run with thread_id for persistence
config = {"configurable": {"thread_id": "user-123-session-456"}}
result = await app.ainvoke({"messages": [HumanMessage(content="Hello")]}, config)

# Resume later with same thread_id
result2 = await app.ainvoke({"messages": [HumanMessage(content="Continue")]}, config)
Enter fullscreen mode Exit fullscreen mode

Part 2: Advanced Graph Patterns

Conditional Routing

# langgraph/routing.py
from typing import Literal

def route_by_intent(state: AgentState) -> Literal["research", "code", "write", "error"]:
    last_msg = state["messages"][-1].content.lower()

    if any(kw in last_msg for kw in ["research", "find", "search", "lookup"]):
        return "research"
    elif any(kw in last_msg for kw in ["code", "implement", "build", "function"]):
        return "code"
    elif any(kw in last_msg for kw in ["write", "document", "explain", "summarize"]):
        return "write"
    return "error"

def route_by_confidence(state: AgentState) -> Literal["proceed", "clarify", "escalate"]:
    confidence = state.get("confidence", 1.0)

    if confidence > 0.8:
        return "proceed"
    elif confidence > 0.5:
        return "clarify"
    return "escalate"

# Multi-condition routing
def complex_router(state: AgentState) -> dict:
    """Return multiple routing decisions."""
    return {
        "next_node": "process",
        "parallel_nodes": ["validate", "enrich"],
        "skip_nodes": [] if state["retry_count"] < 3 else ["optional_step"]
    }
Enter fullscreen mode Exit fullscreen mode

Parallel Execution

# langgraph/parallel.py
from langgraph.graph import StateGraph
from typing: Annotated, List
import operator

class ParallelState(TypedDict):
    messages: Annotated[List[BaseMessage], add_messages]
    research_results: Annotated[List[dict], operator.add]  # Accumulate
    code_results: Annotated[List[dict], operator.add]
    write_results: Annotated[List[dict], operator.add]

async def research_node(state: ParallelState):
    # Run multiple searches in parallel
    import asyncio

    async def search(query):
        return {"query": query, "results": await web_search(query)}

    queries = extract_queries(state["messages"][-1].content)
    results = await asyncio.gather(*[search(q) for q in queries])

    return {"research_results": results}

async def code_node(state: ParallelState):
    # Parallel code generation for different components
    tasks = extract_code_tasks(state["messages"][-1].content)
    results = await asyncio.gather(*[generate_code(t) for t in tasks])
    return {"code_results": results}

# Fan-out/fan-in pattern
workflow = StateGraph(ParallelState)
workflow.add_node("research", research_node)
workflow.add_node("code", code_node)
workflow.add_node("synthesize", synthesize_node)

workflow.add_edge(START, "research")
workflow.add_edge(START, "code")  # Both start from START
workflow.add_edge("research", "synthesize")
workflow.add_edge("code", "synthesize")
workflow.add_edge("synthesize", END)
Enter fullscreen mode Exit fullscreen mode

Human-in-the-Loop

# langgraph/human_in_loop.py
from langgraph.types import interrupt, Command
from dataclasses import dataclass

@dataclass
class HumanReview:
    approved: bool
    feedback: str
    modifications: dict

async def human_review_node(state: AgentState):
    # Present to human for review
    review_request = {
        "task": state["messages"][-1].content,
        "proposed_action": state.get("proposed_action"),
        "context": state.get("context")
    }

    # This pauses execution and waits for human input
    human_response = interrupt(review_request)

    # Resume with human decision
    review = HumanReview(**human_response)

    if review.approved:
        return {"human_approved": True, "modifications": review.modifications}
    else:
        return {
            "human_approved": False,
            "feedback": review.feedback,
            "retry_count": state["retry_count"] + 1
        }

def route_after_review(state: AgentState) -> str:
    if state.get("human_approved"):
        return "execute"
    elif state["retry_count"] >= 3:
        return "escalate"
    return "revise"

# Usage
app = workflow.compile(checkpointer=checkpointer, interrupt_before=["human_review"])

# First invocation - pauses at human_review
result = await app.ainvoke(input_data, config)

# Check if interrupted
if "__interrupt__" in result:
    interrupt_info = result["__interrupt__"]
    # Present to human, get response
    human_decision = await get_human_decision(interrupt_info)

    # Resume with human input
    result = await app.ainvoke(Command(resume=human_decision), config)
Enter fullscreen mode Exit fullscreen mode

Subgraphs (Modularity)

# langgraph/subgraphs.py
from langgraph.graph import StateGraph

# Subgraph for research
research_graph = StateGraph(AgentState)
research_graph.add_node("search", search_node)
research_graph.add_node("summarize", summarize_node)
research_graph.add_edge(START, "search")
research_graph.add_edge("search", "summarize")
research_graph.add_edge("summarize", END)

research_app = research_graph.compile()

# Subgraph for code generation
code_graph = StateGraph(AgentState)
code_graph.add_node("plan", plan_code_node)
code_graph.add_node("implement", implement_node)
code_graph.add_node("test", test_node)
code_graph.add_edge(START, "plan")
code_graph.add_edge("plan", "implement")
code_graph.add_edge("implement", "test")
code_graph.add_edge("test", END)

code_app = code_graph.compile()

# Main graph using subgraphs
main_graph = StateGraph(AgentState)
main_graph.add_node("research", research_app)
main_graph.add_node("code", code_app)
main_graph.add_node("integrate", integrate_node)

main_graph.add_edge(START, "research")
main_graph.add_edge("research", "code")
main_graph.add_edge("code", "integrate")
main_graph.add_edge("integrate", END)

# Compile with shared checkpointer
app = main_graph.compile(checkpointer=checkpointer)
Enter fullscreen mode Exit fullscreen mode

Part 3: Streaming & Real-Time

Token Streaming

# langgraph/streaming.py
from langgraph.graph import StateGraph
from typing: AsyncGenerator

async def stream_graph(app, input_data: dict, config: dict) -> AsyncGenerator[dict, None]:
    """Stream graph execution events."""

    async for event in app.astream_events(input_data, config, version="v2"):
        event_type = event["event"]

        if event_type == "on_chain_start":
            yield {"type": "node_start", "node": event["name"]}
        elif event_type == "on_chain_end":
            yield {"type": "node_end", "node": event["name"], "output": event["data"]["output"]}
        elif event_type == "on_chat_model_stream":
            yield {"type": "token", "content": event["data"]["chunk"].content}
        elif event_type == "on_tool_start":
            yield {"type": "tool_start", "tool": event["name"], "input": event["data"]["input"]}
        elif event_type == "on_tool_end":
            yield {"type": "tool_end", "tool": event["name"], "output": event["data"]["output"]}
        elif event_type == "on_error":
            yield {"type": "error", "error": str(event["data"]["error"])}

# Usage in FastAPI
@app.post("/stream")
async def stream_endpoint(request: QueryRequest):
    async def event_generator():
        async for event in stream_graph(app, request.dict(), config):
            yield f"data: {json.dumps(event)}\n\n"

    return StreamingResponse(event_generator(), media_type="text/event-stream")
Enter fullscreen mode Exit fullscreen mode

Progress Tracking

# langgraph/progress.py
from dataclasses import dataclass
from typing: Dict
import asyncio

@dataclass
class ProgressState:
    current_step: str
    total_steps: int
    completed_steps: int
    step_details: Dict[str, any]

class ProgressTracker:
    def __init__(self):
        self.subscribers: Dict[str, asyncio.Queue] = {}

    async def track(self, thread_id: str, state: AgentState):
        if thread_id not in self.subscribers:
            self.subscribers[thread_id] = asyncio.Queue()

        progress = ProgressState(
            current_step=state.get("current_step", "unknown"),
            total_steps=state.get("total_steps", 10),
            completed_steps=state.get("completed_steps", 0),
            step_details=state.get("step_details", {})
        )

        await self.subscribers[thread_id].put(progress)

    async def subscribe(self, thread_id: str) -> AsyncGenerator[ProgressState, None]:
        queue = asyncio.Queue()
        self.subscribers[thread_id] = queue

        try:
            while True:
                progress = await queue.get()
                yield progress
        finally:
            del self.subscribers[thread_id]

# Inject into graph nodes
async def tracked_node(state: AgentState, tracker: ProgressTracker):
    result = await actual_node_logic(state)
    await tracker.track(state["configurable"]["thread_id"], result)
    return result
Enter fullscreen mode Exit fullscreen mode

Part 4: Error Handling & Resilience

Retry with Exponential Backoff

# langgraph/resilience.py
from langgraph.graph import StateGraph
from typing: Callable
import asyncio
import random

def with_retry(max_retries: int = 3, base_delay: float = 1.0):
    def decorator(func: Callable):
        async def wrapper(state: AgentState):
            for attempt in range(max_retries + 1):
                try:
                    return await func(state)
                except Exception as e:
                    if attempt == max_retries:
                        raise

                    delay = base_delay * (2 ** attempt) + random.uniform(0, 0.1)
                    await asyncio.sleep(delay)

                    # Update retry count
                    return {
                        **state,
                        "retry_count": state.get("retry_count", 0) + 1,
                        "last_error": str(e)
                    }
        return wrapper
    return decorator

@with_retry(max_retries=3)
async def unreliable_tool_node(state: AgentState):
    # Tool that might fail
    return await call_external_api(state["input"])

# Circuit breaker pattern
class CircuitBreaker:
    def __init__(self, failure_threshold: int = 5, timeout: float = 60):
        self.failure_threshold = failure_threshold
        self.timeout = timeout
        self.failures = 0
        self.last_failure = 0
        self.state = "closed"

    async def call(self, func, *args, **kwargs):
        if self.state == "open":
            if time.time() - self.last_failure > self.timeout:
                self.state = "half-open"
            else:
                raise Exception("Circuit breaker open")

        try:
            result = await func(*args, **kwargs)
            if self.state == "half-open":
                self.state = "closed"
                self.failures = 0
            return result
        except Exception as e:
            self.failures += 1
            self.last_failure = time.time()
            if self.failures >= self.failure_threshold:
                self.state = "open"
            raise
Enter fullscreen mode Exit fullscreen mode

Fallback Strategies

# langgraph/fallbacks.py
from typing: Optional

async def node_with_fallback(state: AgentState) -> dict:
    """Try primary, fallback to secondary on failure."""

    try:
        return await primary_llm_call(state)
    except Exception as e:
        # Log failure
        logger.warning(f"Primary failed: {e}, trying fallback")

        try:
            return await fallback_llm_call(state)
        except Exception as e2:
            # Final fallback: structured error response
            return {
                "error": "All models failed",
                "fallback_response": generate_cached_response(state["input"]),
                "details": {"primary": str(e), "fallback": str(e2)}
            }

# Model cascade
async def cascade_models(state: AgentState) -> dict:
    models = ["gpt-4o", "gpt-4o-mini", "claude-3-sonnet", "local-llama"]

    for model in models:
        try:
            result = await call_model(model, state["input"])
            return {"model_used": model, "result": result}
        except Exception as e:
            continue

    raise Exception("All models in cascade failed")
Enter fullscreen mode Exit fullscreen mode

Part 5: Production Deployment

FastAPI Server

# langgraph/server.py
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel
from typing: Optional, Dict, Any
import uuid

app = FastAPI(title="LangGraph Agent API")

class InvokeRequest(BaseModel):
    input: dict
    thread_id: Optional[str] = None
    config: Optional[Dict[str, Any]] = None

class StreamRequest(BaseModel):
    input: dict
    thread_id: Optional[str] = None

@app.post("/invoke")
async def invoke(request: InvokeRequest):
    thread_id = request.thread_id or str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}
    if request.config:
        config.update(request.config)

    try:
        result = await app.state.graph.ainvoke(request.input, config)
        return {"thread_id": thread_id, "result": result}
    except Exception as e:
        raise HTTPException(500, str(e))

@app.post("/stream")
async def stream(request: StreamRequest):
    thread_id = request.thread_id or str(uuid.uuid4())
    config = {"configurable": {"thread_id": thread_id}}

    async def generate():
        async for event in app.state.graph.astream_events(
            request.input, config, version="v2"
        ):
            yield f"data: {json.dumps(event)}\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

@app.get("/state/{thread_id}")
async def get_state(thread_id: str):
    config = {"configurable": {"thread_id": thread_id}}
    state = await app.state.graph.aget_state(config)
    return state.values if state else {}

@app.post("/resume/{thread_id}")
async def resume(thread_id: str, human_input: dict):
    config = {"configurable": {"thread_id": thread_id}}
    result = await app.state.graph.ainvoke(Command(resume=human_input), config)
    return {"result": result}

@app.get("/health")
async def health():
    return {"status": "healthy", "graph": app.state.graph.name}

# Initialize on startup
@app.on_event("startup")
async def startup():
    app.state.graph = create_production_graph()
Enter fullscreen mode Exit fullscreen mode

Docker Deployment

# Dockerfile
FROM python:3.11-slim

WORKDIR /app

# System deps
RUN apt-get update && apt-get install -y \
    gcc postgresql-client \
    && rm -rf /var/lib/apt/lists/*

# Python deps
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# App
COPY . .

# Non-root
RUN useradd -m -u 1000 appuser && chown -R appuser:appuser /app
USER appuser

# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:8000/health || exit 1

CMD ["uvicorn", "server:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"]
Enter fullscreen mode Exit fullscreen mode

Kubernetes Deployment

# k8s/langgraph-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: langgraph-agent
spec:
  replicas: 3
  selector:
    matchLabels:
      app: langgraph-agent
  template:
    metadata:
      labels:
        app: langgraph-agent
    spec:
      containers:
      - name: agent
        image: your-registry/langgraph-agent:v1.0
        ports:
        - containerPort: 8000
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: langgraph-secrets
              key: database-url
        - name: REDIS_URL
          valueFrom:
            secretKeyRef:
              name: langgraph-secrets
              key: redis-url
        - name: OPENAI_API_KEY
          valueFrom:
            secretKeyRef:
              name: langgraph-secrets
              key: openai-key
        resources:
          requests:
            memory: "1Gi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "1000m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 30
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: langgraph-agent
spec:
  selector:
    app: langgraph-agent
  ports:
  - port: 80
    targetPort: 8000
  type: ClusterIP
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: langgraph-agent-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: langgraph-agent
  minReplicas: 3
  maxReplicas: 20
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
Enter fullscreen mode Exit fullscreen mode

Part 6: Testing & Evaluation

Graph Testing

# langgraph/testing.py
import pytest
from langgraph.graph import StateGraph

class GraphTestHarness:
    def __init__(self, graph_fn):
        self.graph_fn = graph_fn

    def create_test_app(self, checkpointer=None):
        return self.graph_fn().compile(checkpointer=checkpointer)

    async def run_test(self, input_data: dict, expected: dict = None):
        app = self.create_test_app()
        config = {"configurable": {"thread_id": "test-" + str(uuid.uuid4())}}

        result = await app.ainvoke(input_data, config)

        if expected:
            assert self._match_result(result, expected), f"Expected {expected}, got {result}"

        return result

    def _match_result(self, actual: dict, expected: dict) -> bool:
        for key, value in expected.items():
            if key not in actual:
                return False
            if isinstance(value, dict):
                if not self._match_result(actual[key], value):
                    return False
            elif actual[key] != value:
                return False
        return True

# Test fixtures
@pytest.fixture
def test_graph():
    return create_test_graph()

@pytest.mark.asyncio
async def test_basic_flow(test_graph):
    harness = GraphTestHarness(lambda: test_graph)
    result = await harness.run_test(
        {"messages": [HumanMessage(content="Hello")]},
        {"current_step": "responded"}
    )
    assert "messages" in result
Enter fullscreen mode Exit fullscreen mode

Property-Based Testing

# langgraph/property_testing.py
from hypothesis import given, strategies as st
import pytest

@given(st.text(min_size=1, max_size=1000))
async def test_input_sanitization(input_text):
    """Graph should handle any input without crashing."""
    app = create_test_graph().compile()
    config = {"configurable": {"thread_id": "property-test"}}

    result = await app.ainvoke({"messages": [HumanMessage(content=input_text)]}, config)

    # Should always produce valid state
    assert "messages" in result
    assert isinstance(result["messages"], list)

@given(st.integers(min_value=0, max_value=10))
async def test_retry_behavior(retry_count):
    """Graph should respect retry limits."""
    state = {"retry_count": retry_count, "messages": []}
    result = await retry_limited_node(state)

    if retry_count >= 3:
        assert result.get("escalated") == True
    else:
        assert result.get("retry_count") == retry_count + 1
Enter fullscreen mode Exit fullscreen mode

Part 7: Observability

LangSmith Integration

# langgraph/observability.py
from langsmith import traceable
from langsmith.run_helpers import get_current_run_tree

@traceable(name="agent_reasoning")
async def reason_node(state: AgentState):
    run_tree = get_current_run_tree()
    if run_tree:
        run_tree.add_metadata({"user_id": state.get("user_id")})

    # Your logic here
    return result

# Custom callbacks
from langgraph.callbacks import BaseCallbackHandler

class ObservabilityCallback(BaseCallbackHandler):
    def on_node_start(self, node_name: str, state: dict):
        # Send to monitoring
        pass

    def on_node_end(self, node_name: str, output: dict):
        # Record metrics
        pass

    def on_error(self, node_name: str, error: Exception):
        # Alert
        pass

app = workflow.compile(
    checkpointer=checkpointer,
    callbacks=[ObservabilityCallback()]
)
Enter fullscreen mode Exit fullscreen mode

Metrics Collection

# langgraph/metrics.py
from dataclasses import dataclass
from datetime import datetime
import time

@dataclass
class NodeMetrics:
    node_name: str
    start_time: float
    end_time: float
    duration_ms: float
    input_tokens: int
    output_tokens: int
    success: bool
    error: str = ""

class MetricsCollector:
    def __init__(self):
        self.metrics: List[NodeMetrics] = []

    def record(self, metric: NodeMetrics):
        self.metrics.append(metric)

    def get_node_stats(self, node_name: str) -> dict:
        node_metrics = [m for m in self.metrics if m.node_name == node_name]
        if not node_metrics:
            return {}

        durations = [m.duration_ms for m in node_metrics]
        return {
            "count": len(node_metrics),
            "avg_duration_ms": sum(durations) / len(durations),
            "p95_duration_ms": sorted(durations)[int(len(durations) * 0.95)],
            "success_rate": sum(1 for m in node_metrics if m.success) / len(node_metrics),
            "total_tokens": sum(m.input_tokens + m.output_tokens for m in node_metrics)
        }

    def get_graph_summary(self) -> dict:
        return {
            "total_nodes": len(set(m.node_name for m in self.metrics)),
            "total_executions": len(self.metrics),
            "overall_success_rate": sum(1 for m in self.metrics if m.success) / len(self.metrics),
            "by_node": {n: self.get_node_stats(n) for n in set(m.node_name for m in self.metrics)}
        }
Enter fullscreen mode Exit fullscreen mode

Decision Framework

LANGGRAPH PATTERN SELECTION:

Simple linear workflow?
├─→ Yes → Basic StateGraph with linear edges
└─→ No
    ├─→ Need cycles/loops?
    │   ├─→ Yes → Conditional edges with retry logic
    │   └─→ No
    │       ├─→ Parallel independent tasks?
    │       │   └─→ Yes → Fan-out/fan-in with Annotated accumulation
    │       │
    │       ├─→ Human review needed?
    │       │   └─→ Yes → interrupt() + Command(resume=...)
    │       │
    │       ├─→ Complex sub-workflows?
    │       │   └─→ Yes → Subgraphs (compile separately, compose)
    │       │
    │       └─→ Production deployment?
    │           └─→ Yes → Checkpointer + Streaming + Metrics + Health checks
Enter fullscreen mode Exit fullscreen mode

Production Checklist

Graph Design

  • [ ] State schema defined with TypedDict
  • [ ] All nodes are async for concurrency
  • [ ] Conditional edges cover all paths
  • [ ] No unreachable nodes
  • [ ] Retry limits on all external calls
  • [ ] Circuit breakers for external APIs

Persistence

  • [ ] Checkpointer configured (Postgres/Redis for prod)
  • [ ] Thread ID strategy defined (user/session)
  • [ ] State migration plan for schema changes
  • [ ] Checkpoint TTL/cleanup policy

Reliability

  • [ ] Timeout on all node executions
  • [ ] Fallback models for LLM calls
  • [ ] Graceful degradation on tool failures
  • [ ] Idempotent operations where possible
  • [ ] Dead letter queue for failed executions

Observability

  • [ ] LangSmith or custom tracing
  • [ ] Node-level metrics (latency, tokens, errors)
  • [ ] Business metrics (task completion, user satisfaction)
  • [ ] Alerts on error rates, latency spikes
  • [ ] Distributed tracing across services

Deployment

  • [ ] Health endpoint returns graph status
  • [ ] Graceful shutdown (finish in-flight)
  • [ ] Horizontal scaling with sticky sessions (thread_id)
  • [ ] Blue-green deployment support
  • [ ] Rollback procedure tested

Testing

  • [ ] Unit tests for each node
  • [ ] Integration tests for full flows
  • [ ] Property-based tests for edge cases
  • [ ] Chaos testing (node failures, timeouts)
  • [ ] Load testing with realistic traffic

Resources


LangGraph makes complex AI workflows manageable. Start simple, add persistence early, instrument everything, and test failure paths as thoroughly as success paths.

Top comments (0)