Storage that survives
A persistent saver, Postgres for concurrent writers, with setup() called at start-up and the right connection settings.
Technical guide
In LangGraph the checkpointer is the agent's memory of a conversation. Every step of a thread is saved through it, and every resume, approval and follow-up message reads from it. When it is misconfigured the symptoms rarely mention it: the agent simply forgets, or works in development and fails under load. These are the failures we have met running checkpointed agents for every Olano deployment, with the fix for each.
After each step of a graph - each model call, each batch of tool calls - LangGraph writes a checkpoint: the full state at that moment, plus pending writes and metadata about which step produced it. Checkpoints are grouped by thread, identified by the thread_id you pass in the run's config. When the next message arrives for that thread, LangGraph loads the latest checkpoint and continues from it. That is the whole mechanism behind conversation memory, interrupts that wait for a person, and resuming after a crash.
It follows that the checkpointer is the source of truth for what the model sees next. Any other record you keep - a transcript for a UI, an analytics log - is a copy, and it can disagree with the checkpoint without anyone noticing until a user asks why the agent remembers something they deleted.
The most common first error. A graph compiled with a checkpointer will not run without a thread to save into:
ValueError: Checkpointer requires one or more of the following
'configurable' keys: thread_id, checkpoint_ns, checkpoint_id
Pass {"configurable": {"thread_id": "..."}} as the config on every call. Choose the id deliberately: it decides what counts as one conversation. One id per customer chat gives each customer continuity; one id per message gives the agent amnesia; one id for everything gives every customer each other's history. On a multi-channel system, include the channel in the id - a WhatsApp number and an email address belonging to the same person are different threads unless you decide otherwise.
InMemorySaver keeps checkpoints in the process's memory. It is right for tests and wrong for anything that restarts, and a deploy is a restart. If memory survives within a session but not across a deploy, check which saver is in use before anything else. The persistent options are SQLite (SqliteSaver, AsyncSqliteSaver) for a single process with modest concurrency, and Postgres (PostgresSaver, AsyncPostgresSaver) when several agents write at once. Olano's production default is Postgres for that reason; SQLite remains available as a zero-configuration option.
No tables. The Postgres saver creates its schema only when you call setup() (or await setup() on the async saver). Call it once at start-up; it is safe to run repeatedly.
The wrong connection settings. If you hand the saver your own connection or pool rather than a connection string, it must be opened with autocommit=True and row_factory=dict_row. Without autocommit the setup and writes sit in an uncommitted transaction; without the dict row factory reads fail on tuple rows. The saver's own from_conn_string sets both for you.
A pool shared across event loops. Covered next, because it is the same bug as SQLite's.
The async savers hold objects tied to the asyncio event loop that first used them: AsyncSqliteSaver holds an asyncio.Lock, and an async Postgres pool binds its connections the same way. Create the saver once at import time, cache it, and then use it from a second loop - a background thread running its own loop, a test framework that creates a fresh loop per test, a worker process - and the second loop fails with a runtime error saying the lock or future is bound to a different event loop.
The fix is to cache per loop, not per process. Key the saver or pool by the running loop as well as by its database URL, and build a new one the first time each loop asks. Close it when that loop's work ends; an ephemeral worker that never closes its connection keeps a SQLite file locked for everyone else. Olano caches its async savers keyed by agent, database and event loop for exactly this reason, and opens every SQLite connection with a generous busy timeout so a brief writer overlap waits instead of failing.
A checkpoint holds the entire conversation, tool results included, so it is as sensitive as anything in your system. LangGraph can encrypt every blob it writes: wrap the serializer with EncryptedSerializer.from_pycryptodome_aes(), which reads a 16, 24 or 32 byte key from the LANGGRAPH_AES_KEY environment variable, and pass it to the saver as serde.
Two decisions come with it. What happens to rows written before encryption was switched on - the stock serializer can still read plaintext rows, which eases migration but means a plaintext row planted in the database would also be accepted. And what happens without the key - fail at start-up, loudly, rather than at the first read. In Olano a deployment can require encryption, in which case a plaintext row is refused rather than decoded and a missing key stops the saver from being built at all; only the one-off migration tool reads legacy plaintext, so old threads move across and land encrypted.
Savers implement delete_thread(thread_id) (and adelete_thread), which removes every checkpoint and pending write for that thread. Call it, but notice what it does not touch: any transcript, search index, summary or analytics row your application wrote alongside. If a person asks for a conversation to be deleted and only the checkpoint goes, the agent forgets it but your dashboard still shows it; if only the transcript goes, the dashboard forgets it but the agent still remembers. In Olano, deleting a thread purges both the checkpoint and the transcript in the same operation, and we treat any store that holds conversation content as part of the delete.
There is no automatic migration between savers. Point a running system from SQLite to Postgres and every existing thread appears empty - the agent has not forgotten anything, it is reading a different database. Migrate deliberately: iterate the old saver's checkpoints with list(), write them to the new one with put() and put_writes(), and verify a sample of threads before switching traffic. If migration is not possible yet, keep the old backend readable rather than leaving users to discover the gap.
Before a checkpointed agent goes to production.
A persistent saver, Postgres for concurrent writers, with setup() called at start-up and the right connection settings.
One id per conversation as your users understand it, including the channel, never per message and never shared.
Async savers and pools cached by loop, closed when the loop's work ends, with a busy timeout on SQLite.
Checkpoint blobs encrypted with a managed key, and a clear rule for legacy plaintext rows.
delete_thread plus every transcript, index or summary your application keeps.
Moving between backends copies checkpoints explicitly; a configuration change alone empties every thread.
A graph compiled with a checkpointer saves state per thread, so every run needs a thread to save into. Pass a config of the form {"configurable": {"thread_id": "..."}} on each call. The id decides what counts as one conversation, so pick it to match how your users think about one.
It is almost always using InMemorySaver, which keeps checkpoints in process memory. Use a persistent saver instead: SqliteSaver or AsyncSqliteSaver for a single process, PostgresSaver or AsyncPostgresSaver when several agents write concurrently.
The saver, or its connection pool, was created on one asyncio event loop and used from another. Cache savers per event loop rather than per process, creating one the first time each loop needs it and closing it when that loop's work is done.
Yes. Wrap the serializer with EncryptedSerializer.from_pycryptodome_aes(), which reads an AES key from the LANGGRAPH_AES_KEY environment variable, and pass it to the saver as serde. Decide separately how legacy plaintext rows are handled and whether a missing key should stop start-up.
Call the saver's delete_thread (or adelete_thread) with the thread id. Then delete every other copy your application keeps, such as transcripts, search indexes and summaries, or the conversation survives somewhere the agent or your users can still see it.
The rest of the LangChain series, and where to go next.
Which layer of the LangChain stack does what inside an Olano agent, and what Olano adds on top.
Debug mode, LangSmith traces, stream modes, state history, loop limits and fake models.
Leaked tokens from nested model calls, garbled replies, missing paragraph breaks and dropped connections.
Content blocks one provider writes and another rejects, missing context windows, and retries that should not happen.
The product view: what survives a restart, a model change and a new thread.
Threads, long-term memory, knowledge and skills, and how they differ.
Checked 13 October 2026 against LangChain 1.4, langchain-core 1.6, LangGraph 1.2 and Deep Agents 0.7. Olano is an independent company that builds on LangChain's open-source libraries; it is not affiliated with or endorsed by LangChain, Inc. Library behaviour changes between releases, and the LangChain documentation is the primary source. If we have described something wrongly, write to support@olano.ai and we will correct this page.
Every Olano deployment comes with checkpoint storage, encryption and deletes handled. Tell us about your agents and we will show you what moving them would involve.