Durability and Deployment: Off the Laptop

What it takes to run a graph in production: durable execution that survives a crash, node-level retries with backoff, error handlers and timeouts, and the two ways to serve a graph — behind your own web framework, or on the LangGraph server. The series finale.

The capstone runs on your laptop. Between there and production is a gap that has little to do with agents and everything to do with running anything real: it has to survive a crash without losing a half-finished conversation, retry a flaky model or API instead of failing, and serve many users at once behind an HTTP endpoint. The good news, and the through-line of this whole series one last time, is that LangGraph’s answers to those are mostly things you already have — the checkpointing that gave you memory is the same mechanism that gives you durability, and the graph being a Runnable is what makes it deployable. This final chapter covers the production concerns, honestly split between what’s verified here and what’s described from a confirmed API. Run against langgraph 1.2.9.

Durable execution, for free

You already have durability. A checkpointer writes a snapshot after every step, so if the process dies three nodes into a graph, the last completed step is saved — and re-invoking on that thread resumes from there rather than restarting. For a support conversation that’s a convenience; for an expensive multi-step task, like the on-call diagnostics or a long research run, it’s the difference between “redo everything” and “pick up where it stopped,” which at scale is the difference between a recoverable blip and a wasted hour of compute. The requirement is a durable checkpointer: InMemorySaver vanishes with the process, so production uses langgraph-checkpoint-postgres (or SQLite for a single node), where the state lives in a database that outlives any one process.

There’s a knob for how durably each step is saved. invoke and stream take a durability parameter — verified in the signature — trading safety against speed: persist synchronously before continuing (safest, slowest), asynchronously in the background (the usual balance), or only at exit (fastest, least safe). The default is the balanced mode, and you tighten it on the paths where losing a step is expensive — anything past the refund gate wants a synchronous write, so a crash can never leave “refunded the money but forgot we did” as a possible state.

Retries, timeouts, and error handlers

A production graph talks to models and APIs that fail intermittently, and you don’t want a transient blip to kill a run. LangGraph puts the controls on the node. add_node accepts — verified in its signature — a retry_policy, an error_handler, and a timeout:

from langgraph.types import RetryPolicy

builder.add_node(
    "call_model",
    call_model,
    retry_policy=RetryPolicy(max_attempts=3, initial_interval=1.0, backoff_factor=2.0, jitter=True),
    timeout=30,
)

RetryPolicy’s fields — verified as max_attempts, initial_interval, backoff_factor, max_interval, jitter, retry_on — are exponential backoff with jitter, the standard shape for retrying a rate-limited or flaky dependency: wait a bit, wait longer each time, and add randomness so a fleet of retrying instances doesn’t thunder in lockstep. The retry_on field scopes retries to the exceptions worth retrying (a timeout, a 503) rather than blindly retrying a bug, which would just burn attempts on something that will never succeed. This node-level retry is one layer; the others you’ve met stack on top. For model failures specifically, the ModelRetryMiddleware and ModelFallbackMiddleware retry the model then fall back to a different one; for tool failures, the error-as-ToolMessage pattern lets the model recover in-loop. Node retries, agent middleware, tool error handling — a defense at every layer that can fail, which is what production resilience actually looks like: not one big try/except, but graceful degradation wherever a call crosses a boundary.

Two ways to serve a graph

A compiled graph is a Runnable — it has invoke and astream — so the simplest deployment is to call it yourself from any web framework:

from fastapi import FastAPI
api = FastAPI()

@api.post("/chat")
async def chat(req: ChatRequest):
    config = {"configurable": {"thread_id": req.session_id}}
    result = await app.ainvoke({"messages": [("user", req.text)]}, config)
    return result["messages"][-1].content

That’s all it takes to put the capstone behind an endpoint: map a user’s session to a thread_id, ainvoke the graph, return the last message. Streaming to the client is astream with a mode from Chapter 10, pushed over Server-Sent Events or a WebSocket. Use the async forms (ainvoke, astream) here, because a web server handles many requests at once and you don’t want one user’s slow model call blocking everyone else’s.

The property that makes this scale is worth pausing on, because it’s the payoff of everything in Act 2. Because the checkpointer holds all the state, the web process is stateless. Nothing about a conversation lives in the Python process — it’s all in the checkpointer’s database, keyed by thread_id. So any server instance can handle any request for any thread: a user’s first message can hit instance A, their reply five minutes later can hit instance B (after A has restarted, even), and it just works, because both instances read the same saved state. That’s what lets you scale horizontally — run ten instances behind a load balancer, point them all at one Postgres checkpointer, and there’s no session affinity to manage, no sticky routing, no in-memory state to lose on a deploy. The stateless-web-plus-shared-state-store pattern is old and well-understood; LangGraph gets you there by default, because the state was never in the process to begin with.

The second option is the LangGraph Server (the platform), which packages all of the above: you describe your graphs in a langgraph.json, run the server, and get a persistent service with a threads API, streaming endpoints, a built-in checkpointer, background runs, and a Studio UI for inspecting traces. It’s the batteries-included path when you’d rather not hand-roll the FastAPI layer and thread management yourself.

Verification note, stated plainly: the LangGraph Server and its CLI (langgraph dev / langgraph.json) are described here, not run — deploying the platform is beyond a local verification project, and its langgraph-cli was not installed for this series. The durability parameter, RetryPolicy, and the node-level retry_policy/timeout/error_handler arguments are confirmed against the pinned API; the self-hosted FastAPI pattern is standard Runnable usage. Treat the Server’s specifics as pointers to its own docs, current as of your deploy date, not as run-verified claims — the same honesty this series has applied throughout.

The production checklist

Taking a LangGraph system to production, concretely:

  • Swap InMemorySaver for a durable checkpointer (Postgres), so state and durability survive restarts, and set durability tighter on irreversible paths.
  • Put retry policies on nodes that call flaky dependencies, and fallback/retry middleware on the model.
  • Keep human gates (interrupts) on irreversible actions — they work in production exactly as locally, because a pause is just a checkpoint that can now wait across process restarts.
  • Add tracing (Chapter 16) so you can see production runs, and fake-model tests in CI so control-flow regressions never ship.
  • Serve behind your own async framework or the LangGraph Server, with the checkpointer as the shared state store so instances stay stateless and scale out.

None of these are new systems. They’re the primitives you already know, configured for a hostile environment.

Final thoughts

Production durability is the checkpointer you’ve had since Act 2, made durable with a real database and tuned with a durability mode; resilience is retry policies, middleware, and tool error handling stacked at every layer that can fail; and deployment is either a few lines of async FastAPI over a Runnable or the batteries-included LangGraph Server, scaling out because the state was never in the process. The through-line holds right to the end: there is no separate “production LangGraph” to learn, only the graph you built, pointed at a durable store and wrapped in an endpoint.

That closes the book. We started with a model that couldn’t act and the agent loop that fixes it, and the discovery that create_agent is just a graph; we built the primitives, then the graph from an empty constructor up — state, routing, memory, human pauses, streaming; then multi-agent systems and a capstone that ties it together and runs. The one idea underneath all of it: an agent is a graph you can see, draw, and edit. Once that’s true for you, there’s no magic left — only nodes, edges, and state, arranged with judgment. Go build one.

Comments