LangGraph Checkpointing with Redis
LangGraph checkpointing with Redis gives you durable, shared state for agent graphs without reaching for a full database. If you have ever restarted a long-running agent process and lost every in-flight conversation, or tried to run more than one worker against the same graph and watched state go out of sync, this is the fix. LangGraph's checkpointer interface is built exactly for this: swap the default in-memory saver for a Redis-backed one and your graph's state, message history, and pending interrupts all live outside the process.
This article walks through what a LangGraph checkpoint actually contains, how to install and configure the Redis checkpointer, how threads and checkpoint IDs work together, and how to use this for human-in-the-loop workflows, multi-worker deployments, and time travel debugging. Everything below assumes you already have a working LangGraph graph and just need durable state.
Why checkpointing matters for LangGraph agents
A LangGraph graph is a state machine. Every node reads the current state, does some work (often an LLM call or a tool call), and returns a state update. The checkpointer is the component that persists that state after every "superstep" (a round of node execution). Without a checkpointer, or with the default in-memory one, all of that state disappears the moment your process exits.
That matters for a few concrete reasons:
- Long-running or paused agents. If your agent uses
interrupt()to pause and wait for human approval before calling a sensitive tool, that pause can last minutes or days. The state has to live somewhere that isn't the process's heap. - Multi-worker deployments. If you run your agent behind a web server with multiple worker processes (Gunicorn, uvicorn workers, or a queue-based worker pool), a user's next message might land on a different process than the one that handled their last turn. Shared state is the only way that works.
- Crash recovery. If a worker dies mid-run, you want to resume from the last completed step, not replay the whole conversation or lose it.
- Time travel and debugging. LangGraph checkpoints are versioned per thread. With a persistent checkpointer, you can list every checkpoint for a thread and rewind to any of them, which is invaluable for debugging why an agent made a bad decision three steps ago.
Redis is a good fit for this because checkpoint reads and writes are on the hot path of every agent turn. You want sub-millisecond latency, not a round trip to a relational database with connection pool contention. Redis also gives you TTLs for free, so stale conversation state can expire on its own instead of requiring a cleanup job.
How LangGraph checkpoints are structured
Before wiring up Redis, it helps to know what a checkpointer actually stores. Every checkpoint is keyed by two things:
- thread_id: identifies a single conversation or run. This is the unit you resume, branch, or inspect.
- checkpoint_id: identifies a specific snapshot within that thread. Each superstep produces a new checkpoint_id, so a thread accumulates a history of checkpoints over time.
A checkpoint itself holds:
- The full graph state at that point (whatever your
StateGraphschema defines: messages, scratch variables, tool outputs, etc.) - Pending writes for nodes that haven't run yet in the current superstep
- Metadata: which node produced the checkpoint, the step number, and any custom metadata you attach
When you call graph.invoke(input, config={"configurable": {"thread_id": "abc123"}}), LangGraph looks up the latest checkpoint for abc123, resumes from there, runs the next superstep, and writes a new checkpoint. If you never pass a thread_id, checkpointing is effectively a no-op per call.
There is also a second, related concept: the store. Checkpointers persist per-thread run state. A store (also pluggable, and Redis-backed store implementations exist too) persists longer-lived memory that spans threads, like user preferences. This article focuses on the checkpointer, since that is what most people mean by "LangGraph checkpointing with Redis."
Installing the Redis checkpointer
LangGraph ships checkpointer backends as separate packages so your core install stays light. For Redis:
pip install langgraph langgraph-checkpoint-redis redisThe langgraph-checkpoint-redis package provides RedisSaver (synchronous) and AsyncRedisSaver (async), both implementing the same BaseCheckpointSaver interface the in-memory and Postgres savers implement. That means swapping backends later is a one-line change, not a rewrite.
You need a running Redis instance. For local development:
docker run -d --name langgraph-redis -p 6379:6379 redis:7-alpineFor production, use a managed Redis (ElastiCache, Redis Cloud, Upstash, or a self-hosted cluster with persistence enabled). Since checkpoints are your source of truth for in-flight agent state, do not point this at an ephemeral cache instance with no persistence and no backups.
Wiring RedisSaver into a graph
Here is a minimal example: a two-node graph that calls a model, then a tool, with Redis-backed checkpointing.
from langgraph.graph import StateGraph, START, END
from langgraph.checkpoint.redis import RedisSaver
from typing import TypedDict, Annotated
from operator import add
class AgentState(TypedDict):
messages: Annotated[list, add]
step_count: int
def call_model(state: AgentState) -> AgentState:
# placeholder for an actual LLM call
reply = f"Processed {len(state['messages'])} messages"
return {"messages": [reply], "step_count": state.get("step_count", 0) + 1}
def call_tool(state: AgentState) -> AgentState:
return {"messages": ["tool result"], "step_count": state["step_count"] + 1}
builder = StateGraph(AgentState)
builder.add_node("model", call_model)
builder.add_node("tool", call_tool)
builder.add_edge(START, "model")
builder.add_edge("model", "tool")
builder.add_edge("tool", END)
with RedisSaver.from_conn_string("redis://localhost:6379") as checkpointer:
checkpointer.setup() # creates required Redis indices, run once per deployment
graph = builder.compile(checkpointer=checkpointer)
config = {"configurable": {"thread_id": "user-42-session-1"}}
result = graph.invoke({"messages": ["hello"], "step_count": 0}, config)
print(result)A few details that trip people up the first time:
`setup()` is not optional. RedisSaver uses RediSearch indices under the hood to make checkpoint lookups efficient. If you skip setup(), either the writes will fail or lookups will fall back to slow scans depending on the version. Call it once when your deployment starts, not on every request. If you are running multiple app instances, guard it so only one instance runs setup, or make it idempotent in your deploy script.
Context manager vs long-lived client. The with RedisSaver.from_conn_string(...) pattern closes the connection when the block exits, which is wrong for a long-running server. For a web app, construct the saver once at startup and keep it open for the process lifetime:
from langgraph.checkpoint.redis import RedisSaver
redis_saver = RedisSaver.from_conn_string("redis://localhost:6379")
redis_saver.setup()
graph = builder.compile(checkpointer=redis_saver)Store redis_saver and graph at module scope or in your app's dependency container, and reuse them across requests.
Async apps should use `AsyncRedisSaver`. If your server is built on FastAPI or another async framework, use the async variant and await checkpointer.asetup(), then call graph.ainvoke(...) instead of graph.invoke(...). Mixing sync checkpointer calls into an async event loop will block it.
from langgraph.checkpoint.redis.aio import AsyncRedisSaver
async def build_graph():
checkpointer = AsyncRedisSaver.from_conn_string("redis://localhost:6379")
await checkpointer.asetup()
return builder.compile(checkpointer=checkpointer)Configuring connection details and TTLs
from_conn_string accepts a standard Redis URL, so you can pass auth, a specific database index, or TLS settings the same way you would to any Redis client:
redis_saver = RedisSaver.from_conn_string(
"rediss://default:yourpassword@your-redis-host:6380/0"
)For production you almost always want a TTL on checkpoints. Without one, every conversation your app ever handles accumulates in Redis forever, and Redis is memory-backed, so that gets expensive fast. RedisSaver supports a TTL configuration at construction time:
from langgraph.checkpoint.redis import RedisSaver
redis_saver = RedisSaver.from_conn_string(
"redis://localhost:6379",
ttl={
"default_ttl": 60 * 60 * 24, # 24 hours, in seconds
"refresh_on_read": True, # extend TTL when a checkpoint is read
},
)
redis_saver.setup()refresh_on_read matters for anything with a human-in-the-loop pause: if a user might come back to a paused thread after 12 hours, you want the TTL to reset on access, not expire mid-conversation just because the clock ran out while they were reading an approval prompt.
If you need certain threads to persist indefinitely (e.g., you archive completed support tickets), don't rely on Redis TTLs for those. Either write a background job that copies finished threads out to durable storage, or use a mixed strategy: Redis for active threads, a Postgres checkpointer or your own archive table for anything you need to keep long-term.
Human-in-the-loop with persistent checkpoints
This is where Redis-backed checkpointing earns its keep. LangGraph's interrupt() function pauses graph execution and returns control to the caller, and the graph does not resume until you explicitly send a Command(resume=...) with the same thread_id.
from langgraph.types import interrupt, Command
def approval_node(state: AgentState) -> AgentState:
decision = interrupt({"action": "delete_records", "count": 500})
if decision != "approved":
return {"messages": ["Action cancelled"]}
return {"messages": ["Action approved, proceeding"]}When interrupt() fires, LangGraph writes a checkpoint capturing exactly where execution stopped. With RedisSaver, that checkpoint is durable. Your web server can return a "pending approval" response to the user, shut down, redeploy, or route the next request to a completely different process, and none of it matters, because the state lives in Redis, not in the worker's memory.
Resuming looks like this, and it can happen from any process that shares the same Redis instance:
config = {"configurable": {"thread_id": "user-42-session-1"}}
result = graph.invoke(Command(resume="approved"), config)This pattern is what makes approval queues, review dashboards, and Slack-based approval bots practical: the approval UI and the agent worker don't need to be the same process, or even the same service, as long as they point at the same Redis instance and the same thread_id.
Running multiple workers against the same graph
If you deploy your LangGraph app behind several worker processes (say, four uvicorn workers behind a load balancer), Redis checkpointing is what keeps a user's conversation coherent regardless of which worker handles which request. The graph object itself can be compiled independently in each worker; what needs to be shared is the Redis connection.
Two things to watch for in this setup:
Idempotent `setup()`. Have exactly one process run checkpointer.setup(), typically in a deploy hook or an init container, rather than every worker running it on boot. setup() is generally safe to call more than once, but avoid the race of four workers hitting it simultaneously at cold start.
Thread ID ownership. Two workers should never process the same thread_id concurrently. If your queueing or routing layer can send concurrent requests for the same thread, add your own lock (a Redis SETNX-based lock is simplest) around the graph.invoke call for that thread. LangGraph's checkpointer will not stop you from writing a conflicting checkpoint if two workers race on the same thread at the same time.
Time travel: inspecting and rewinding checkpoints
Because Redis stores every checkpoint in a thread's history, not just the latest one, you can list them and replay from any point. This is the fastest way to debug an agent that went off the rails.
config = {"configurable": {"thread_id": "user-42-session-1"}}
for checkpoint in graph.get_state_history(config):
print(checkpoint.config["configurable"]["checkpoint_id"], checkpoint.metadata)To resume from a specific past checkpoint instead of the latest one, pass its checkpoint_id explicitly:
rewind_config = {
"configurable": {
"thread_id": "user-42-session-1",
"checkpoint_id": "1ef4f7a0-...",
}
}
graph.invoke(None, rewind_config)This effectively forks the thread from that point. Combined with the fact that checkpoints are cheap to store in Redis, this is a solid pattern for "let me try a different tool call from step 3" style debugging or for building an undo feature into an agent UI.
Choosing between Redis, Postgres, and SQLite checkpointers
LangGraph ships checkpointer implementations for SQLite, Postgres, and Redis, and community backends exist for others. A quick decision guide:
- SQLite: single-process prototypes, notebooks, local testing. Not for anything with concurrent workers.
- Postgres: you already run Postgres, you want transactional guarantees, or you need checkpoints to live alongside other relational data (users, billing, audit logs) with join queries across them.
- Redis: you need the lowest possible latency on checkpoint reads/writes, you're already running Redis for caching or pub/sub, or your checkpoint volume is high enough that a relational database's write throughput becomes a bottleneck under many concurrent threads.
A common production pattern is Redis for active, hot threads (with a TTL) and a periodic export to Postgres or object storage for anything that needs to be queryable or kept indefinitely. Don't treat this as an either/or decision if your durability requirements differ between "agent is mid-conversation" and "conversation is closed and needs to be auditable a year from now."
Common pitfalls
Forgetting `thread_id` entirely. If you omit configurable.thread_id from the config, LangGraph treats the run as stateless and checkpointing has nothing to attach to. Every symptom of "checkpointing isn't working" traces back to this in a surprising number of cases.
Reusing a thread_id across unrelated conversations. Thread IDs should map one-to-one with a conversation or run. If you reuse "default" as a thread_id across every user, in a multi-tenant Redis-backed deployment, you'll get one shared, corrupted conversation history.
Not calling `setup()` after upgrading `langgraph-checkpoint-redis`. Index schemas can change between versions. After a package upgrade, re-run setup() in a staging environment before rolling to production, and check your Redis logs for indexing errors on first write.
Treating Redis as infinite. Checkpoints containing full message histories and large tool outputs can bloat quickly. Set a TTL, and consider trimming messages in your state schema (keep the last N turns, summarize older ones) rather than storing the entire conversation in every checkpoint.
FAQ
Does LangGraph require Redis, or is it optional? Optional. LangGraph's default checkpointer is in-memory and works fine for local development or single-process scripts. Redis, Postgres, and SQLite checkpointers are separate installable packages you add only when you need durability, concurrency, or human-in-the-loop pauses that outlive a single process.
Can I use Redis Cluster instead of a single Redis instance? Yes, RedisSaver.from_conn_string accepts cluster-aware connection strings, and the checkpointer works against Redis Cluster deployments. Verify your specific version's compatibility with RediSearch in cluster mode, since checkpoint lookups rely on secondary indices, and confirm your cluster module set includes RediSearch (or use RedisJSON-only mode if your deployment doesn't support it, checking the package's current documentation for supported modes).
How do I migrate existing checkpoints from SQLite or Postgres to Redis? There's no built-in migration command. The practical approach is to read each thread's state via get_state_history on the old checkpointer, then replay those states into the new RedisSaver using checkpointer.put() for each checkpoint, preserving thread_id and checkpoint_id. For most teams, it's simpler to let old threads finish on the old backend and start new threads on Redis rather than migrating in place.
What happens if Redis goes down mid-conversation? In-flight graph.invoke() calls will raise a connection error, the same as any Redis-dependent service losing its backend. No checkpoint is written for the failed superstep, so on retry (once Redis is back), the graph resumes from the last successfully written checkpoint, not from scratch. Design your calling code to retry invoke on connection errors rather than silently swallowing them.
Do I need both a checkpointer and a store? No, they solve different problems. The checkpointer persists per-thread execution state and is required for resuming a specific run. The store persists cross-thread memory, like a user's stated preferences, that should be available to any future thread for that user. Many apps use a Redis checkpointer alone and add a store only once they need memory that spans conversations.
Is Redis checkpointing safe for sensitive data like PII or approval payloads? Treat it the same as any other data store holding sensitive data: enable TLS on the connection string (rediss://), enable auth, and make sure your Redis instance isn't reachable from outside your private network. LangGraph doesn't encrypt checkpoint contents itself, so if your state includes secrets, encrypt them before they go into the state object, not after.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.