LangGraph Checkpointing in Production
LangGraph checkpointers decide whether your agent survives a crash or deploy. Postgres vs Redis vs SQLite tradeoffs and HITL interrupt/resume patterns.
Most LangGraph demos run in a Jupyter notebook where the process, the state, and the LLM call all live in the same memory space. The graph runs to completion or dies, and either way nobody notices. Production is different. Your agent runs for eleven minutes, calls nine tools across three models, and then your deployment pipeline restarts the container. Without checkpointing, that run is gone — the user sees a timeout, your task queue sees a failure, and you re-pay the token cost from zero. With checkpointing, the graph resumes mid-run at the exact node where it stopped.
Checkpointing is not an optional nice-to-have in LangGraph. It is the mechanism that turns a state machine demo into a durable system. This guide covers the checkpointer architecture, how to choose between Postgres, Redis, and SQLite, the interrupt/resume pattern for human-in-the-loop review, and the failure-recovery runbook we use on our own agent pipelines.
How Checkpointers Actually Work
A LangGraph checkpointer is a BaseCheckpointSaver implementation that persists the graph's state after every "super-step" — one full pass through the node's execution cycle. Conceptually it stores three things per checkpoint:
- Channel values — the current values of every state channel in your graph. This is the state your reducer functions produce.
- A version map — which channel versions each written value corresponds to, so the framework can compute deltas and resume incrementally rather than replaying from scratch.
- Pending tasks — when a node fans out to parallel branches or interrupts, the checkpointer records what has not finished yet.
Each checkpoint is identified by a thread_id (your conversation or job ID) and a monotonically increasing checkpoint number within that thread. You attach a checkpointer at compile time:
from langgraph.checkpoint.postgres import PostgresSaver
with PostgresSaver.from_conn_string(DB_URL) as checkpointer:
checkpointer.setup() # creates checkpoint tables, idempotent
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": job_id}}
result = graph.invoke(initial_state, config=config)
That is the entire happy path. The statefulness problem reappears when you ask the harder questions: what happens on crash, on concurrent writes, on schema change, and when a human needs to approve a step before the graph continues. Those questions are what the rest of this guide answers.
Postgres vs Redis vs SQLite: The Real Tradeoffs
LangGraph ships savers for all three, plus async variants (AsyncPostgresSaver, AsyncRedisSaver, AsyncSqliteSaver). The choice is not about feature checklists — all three store the same checkpoint structure. It is about durability, concurrency, and ops surface.
SQLite: the local default, and its ceiling
SQLite is the right choice for a single-process agent, a CLI tool, or a development environment. It is zero-ops, survives restarts, and gives you real durability via WAL mode. Its ceiling is concurrency: SQLite serializes writers, so multiple worker processes resuming threads against the same file will contend on the write lock. If your agent fleet runs more than a handful of concurrent threads, you will see checkpoint write latency spike exactly when your queue backs up. Use SQLite to develop and to ship demos. Do not use it as the state store behind a multi-worker task queue.
Redis: speed, TTLs, and the durability question
Redis is the fastest option by a wide margin — sub-millisecond checkpoint writes — and the natural fit if your agent already lives near a Redis instance used for queues or caching. Two caveats matter. First, durability: unless you run Redis with AOF appendfsync everysec (or better), a crash can lose the last second of checkpoints. For a short-lived agent run that is usually acceptable; for a long-running HITL workflow where a human reviewer might come back hours later, it is a real risk. Second, memory: checkpoint state accumulates. Long-running threads with large message histories will grow Redis memory unboundedly unless you set an eviction policy or sweep old thread_ids. Redis is a cache that can be configured into a store; treat checkpoint data accordingly.
Postgres: the production default
Postgres is where we land for anything that must survive restarts, deploys, and human review windows. It gives you ACID checkpoint writes, row-level concurrency, point-in-time recovery as a free side effect of your existing backups, and — critically — the ability to inspect state. When a run misbehaves, being able to SELECT the checkpoint blobs, decode the channel values, and see exactly what the agent believed at step 7 is worth more than any speed difference. The cost is latency (a Postgres checkpoint write is a network round trip plus fsync, typically 2-10ms) and schema migration discipline when LangGraph bumps its saver schema — .setup() handles new tables idempotently, but major version upgrades deserve a staged rollout.
Rule of thumb: SQLite for local dev, Redis for high-throughput ephemeral agents with AOF persistence on, Postgres for everything that a human or a cron job might need to resume tomorrow.
Human-in-the-Loop: Interrupt, Review, Resume
The interrupt/resume pattern is where checkpointing pays for itself. LangGraph lets you interrupt before or after any node. The graph serializes its state to the checkpointer and exits; your application layer surfaces the pending state to a human; when the human approves, edits, or rejects, you resume from the exact same checkpoint with new input.
builder.add_node("draft_response", draft_response)
builder.add_node("send_response", send_response)
# Interrupt BEFORE the irreversible node
builder.add_edge("draft_response", "send_response")
graph = builder.compile(
checkpointer=checkpointer,
interrupt_before=["send_response"],
)
# First pass: runs draft_response, stops at send_response
state = graph.invoke({"ticket": ticket}, config=config)
# ... hours later, a reviewer approves in your UI ...
graph.update_state(config, {"approved": True, "reviewer": "josh"})
result = graph.invoke(None, config=config) # resumes at send_response
Three production lessons from running this pattern at scale:
- Interrupt before irreversible actions, not after expensive ones. The natural place to put a human gate is just before the node that sends an email, posts a comment, or moves money — anything you cannot unsend. Interrupting after a long retrieval chain just to review a draft wastes reviewer time; interrupt before the action and include the draft in the interrupt payload.
- Timeouts need a policy, not a hope. An interrupted thread holds state forever if no one reviews it. Sweep stale threads on a schedule and either auto-approve with a confidence threshold, escalate, or fail the job with a clear reason in your task queue. Silent limbo is the failure mode nobody dashboards until it bites.
- Resume is a replay, and replays re-run tool calls unless you make them idempotent. If your graph interrupted after a
create_invoicecall and you resume, some node topologies will re-execute the node. Wrap side-effecting tools in idempotency keys keyed on the thread_id and node name so a replay is a no-op instead of a duplicate charge.
The Failure-Recovery Runbook
Checkpointing narrows the failure window but does not eliminate it. Here is the runbook we actually use when a checkpointed agent pipeline fails in production.
1. Classify the failure first
Not all failures deserve a resume. Three buckets: transient (API timeout, rate limit, network blip) — retry with backoff from the last checkpoint; deterministic (bad input, tool schema mismatch, prompt bug) — fix the code or the input, then resume, because retrying unchanged will fail identically; poison (state itself is corrupt, e.g. a malformed message in the channel values) — fork a new thread with repaired initial state, because resuming from a corrupt checkpoint propagates the corruption.
2. Retry from checkpoint, not from zero
With a thread_id per job, recovery is one call: graph.invoke(None, config=config) with no new input. The framework loads the latest checkpoint, rehydrates channel values, and continues at the pending node. Log the checkpoint number at job start and end so your retry path can assert it advanced — if a retry returns the same checkpoint number, the node threw before writing, and you are in the deterministic bucket.
3. Version your graph with your checkpoints
A checkpoint stores state, not code. If you deploy a graph change while long-running threads are interrupted, resuming those threads runs new code against old state. That is usually fine for additive state channels and catastrophic for removed or renamed ones. Treat graph structure changes like database migrations: bump a version in your graph config, keep backward-compatible reducers for one release, and quarantine threads interrupted before the deploy until you verify they resume cleanly.
4. Monitor checkpoint health, not just job health
The metrics that catch checkpoint problems early are: checkpoint write latency p95 (spikes mean DB contention), threads interrupted longer than your review SLA (limbo), resume-after-crash success rate (below ~99% means your "transient" bucket is misclassified), and checkpoint store growth per day (unbounded growth means no sweep policy). All four are cheap to emit and all four fail silently until they do not.
What Checkpointing Does Not Solve
Checkpointing gives you durable state, not correctness. It will faithfully resume a graph whose prompt was quietly wrong, whose tool output was hallucinated, or whose reducer dropped a field. Pair it with trace logging — we diff JSONL traces from agent runs to find which tool call went wrong — and with cost accounting, since resumed runs re-bill only the nodes that actually re-execute, which makes checkpointing one of the cheapest token-optimization moves you can ship. If you are still choosing a framework, our comparison of LangGraph vs AutoGen for production use covers where checkpointing fits in the broader durability story.
The summary version: pick Postgres unless you have a measured reason not to, interrupt before irreversible nodes, make side effects idempotent, and version your graph like a schema. Do those four things and your agents stop being demos that die on deploy and start being infrastructure you can reason about at 3 a.m.