Closes #8531. `PostgresSaver` and `AsyncPostgresSaver` inherited `prune` / `aprune` from the base class, which raises `NotImplementedError`, so long-lived threads had no supported way to drop old checkpoints. This adds both strategies. Thanks to @wangjiawei-vegetable for proposing this scope in #8531 and #9247. - **`delete`** removes every checkpoint, write and blob of the threads, in one transaction. - **`keep_latest`** keeps the latest checkpoint of each namespace (the one `get_tuple` loads), with its pending writes and the blobs it references, and deletes the rest, including blobs only the deleted checkpoints used. A kept checkpoint whose parent is deleted is left with no parent. A subgraph call's namespace goes with the root checkpoint the call started from; a `checkpointer=True` subgraph keeps its latest checkpoint, since its state outlives each call. - **Each thread is pruned on one snapshot.** Its transaction runs at `REPEATABLE READ` and deletes everything that snapshot sees except the kept checkpoints, so a run appending to the thread at the same time is safe, whatever its checkpoint IDs. - **`keep_latest` refuses threads that use a `DeltaChannel`** (`NotImplementedError` naming them, nothing pruned). A `DeltaChannel` rebuilds its latest value from older checkpoints, so deleting them would empty it. The saver can't see channel types, so it detects one by what only a `DeltaChannel` leaves: `counters_since_delta_snapshot` in metadata, a snapshot (an inline `true` with a blob behind it), or `delta_writes_versioned`, which LangGraph.js sets on threads its `DeltaChannel` graphs start. The check runs for the whole batch first and again in each thread's transaction, so a thread that starts using a `DeltaChannel` mid-batch is refused at its turn. Unlike the `prune` in #6883, `keep_latest` here deletes the blobs older checkpoints no longer share with the kept ones (most of the storage on a long thread), and guards `DeltaChannel` threads. Measured locally, with or without planner statistics: a 6,000-checkpoint thread prunes in 100 to 200 ms, and one with 1,500 finished subgraph calls in about 110 ms. ## Known limitations - Two prunes of the same thread at once can fail one of them with a serialization failure or a deadlock; retrying is safe. - Forking or replaying from a checkpoint while it's being pruned is not safe. - Threads written by older releases can escape the `DeltaChannel` check: before #9142 (Python 1.2.13) and langchain-ai/langgraphjs#2973 (JS 1.5.0), some `update_state` paths saved checkpoints with neither counters nor a snapshot. - Postgres reuses the freed space but doesn't shrink the table files; that takes `VACUUM FULL` or `pg_repack`. - `DeltaChannel`-aware pruning (keeping ancestors back to the last snapshot) is a follow-up. ## Test plan - [x] `libs/checkpoint-postgres/tests/test_prune.py`: 71 tests over the sync and async savers (single connection, pool, pipeline), including the conformance `prune` capability. Passes on Postgres 15 and 16. - [x] `libs/langgraph/tests/test_prune_postgres.py`: 39 graph tests. A pruned thread loads the same state and keeps running like an unpruned twin (sync, async and exit durability, interrupts, a failed parallel step, interrupted and `checkpointer=True` subgraphs), plus deterministic races: a turn saved mid-prune, and threads switching to a `DeltaChannel` before and after their own check. - [x] Each guard (the snapshot, the writes and blob scopes, the subgraph call rules, the parent reset, the `DeltaChannel` checks, the single-string check, the pre-v4 pending sends, the pipeline sync) broken one at a time: every break fails at least one test. - [x] A thread pruned in a loop while it runs, in each durability mode, ends identical to its twin; threads written by LangGraph.js 1.5.0 and pruned from Python behave like their twins.
LangGraph Checkpoint Postgres
To help you ship LangGraph apps to production faster, check out LangSmith. LangSmith is a unified developer platform for building, testing, and monitoring LLM applications.
Quick Install
uv add langgraph-checkpoint-postgres
🤔 What is this?
This library provides a Postgres implementation of LangGraph's checkpoint saver. Use it when you want LangGraph state persistence backed by Postgres for durable, long-running workflows and agents.
By default, langgraph-checkpoint-postgres installs psycopg (Psycopg 3) without any extras. You can choose a specific installation that best suits your needs in the Psycopg installation docs, for example psycopg[binary].
📖 Documentation
For full documentation, see the API reference. For conceptual guides on persistence and memory, see the LangGraph Docs.
Security
Important
Set
LANGGRAPH_STRICT_MSGPACK=trueor pass an explicitallowed_msgpack_moduleslist when creating your checkpointer. This restricts checkpoint deserialization to known-safe types, preventing code execution if the database is compromised. See the langgraph-checkpoint README for details.
Usage
Important
When using Postgres checkpointers for the first time, make sure to call
.setup()method on them to create required tables. See example below.
Important
When manually creating Postgres connections and passing them to
PostgresSaverorAsyncPostgresSaver, make sure to includeautocommit=Trueandrow_factory=dict_row(from psycopg.rows import dict_row). See a full example in this how-to guide.Why these parameters are required:
autocommit=True: Required for the.setup()method to properly commit the checkpoint tables to the database. Without this, table creation may not be persisted.row_factory=dict_row: Required because the PostgresSaver implementation accesses database rows using dictionary-style syntax (e.g.,row["column_name"]). The defaulttuple_rowfactory returns tuples that only support index-based access (e.g.,row[0]), which will causeTypeErrorexceptions when the checkpointer tries to access columns by name.Example of incorrect usage:
# ❌ This will fail with TypeError during checkpointer operations with psycopg.connect(DB_URI) as conn: # Missing autocommit=True and row_factory=dict_row checkpointer = PostgresSaver(conn) checkpointer.setup() # May not persist tables properly # Any operation that reads from database will fail with: # TypeError: tuple indices must be integers or slices, not str
from langgraph.checkpoint.postgres import PostgresSaver
write_config = {"configurable": {"thread_id": "1", "checkpoint_ns": ""}}
read_config = {"configurable": {"thread_id": "1"}}
DB_URI = "postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable"
with PostgresSaver.from_conn_string(DB_URI) as checkpointer:
# call .setup() the first time you're using the checkpointer
checkpointer.setup()
checkpoint = {
"v": 4,
"ts": "2024-07-31T20:14:19.804150+00:00",
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"channel_values": {"my_key": "meow", "node": "node"},
"channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
"versions_seen": {
"__input__": {},
"__start__": {"__start__": 1},
"node": {"start:node": 2},
},
}
# store checkpoint
checkpointer.put(write_config, checkpoint, {}, {})
# load checkpoint
checkpointer.get(read_config)
# list checkpoints
list(checkpointer.list(read_config))
Async
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
async with AsyncPostgresSaver.from_conn_string(DB_URI) as checkpointer:
checkpoint = {
"v": 4,
"ts": "2024-07-31T20:14:19.804150+00:00",
"id": "1ef4f797-8335-6428-8001-8a1503f9b875",
"channel_values": {"my_key": "meow", "node": "node"},
"channel_versions": {"__start__": 2, "my_key": 3, "start:node": 3, "node": 3},
"versions_seen": {
"__input__": {},
"__start__": {"__start__": 1},
"node": {"start:node": 2},
},
}
# store checkpoint
await checkpointer.aput(write_config, checkpoint, {}, {})
# load checkpoint
await checkpointer.aget(read_config)
# list checkpoints
[c async for c in checkpointer.alist(read_config)]
📕 Releases & Versioning
See our Releases and Versioning policies.
💁 Contributing
As an open-source project in a rapidly developing field, we are extremely open to contributions, whether it be in the form of a new feature, improved infrastructure, or better documentation.
For detailed information on how to contribute, see the Contributing Guide.