Steve Wall

InMemorySaver vs SqliteSaver: Which Survives an Approval Wait?
Choose InMemorySaver when a local LangGraph test can lose its state when the process ends. Choose file-backed SqliteSaver when a synchronous development prototype must find the same pending approval in a new process. SqliteSaver.from_conn_string(":memory:") does not solve that second job: a new database connection starts with an empty in-memory database.
This comparison is for developers selecting local prototype storage, not a recommendation for distributed production approval systems. LangChain's checkpoint API reference places the in-memory saver in testing and SQLite in lightweight projects. Its production memory guidance points to database-backed production checkpointers. We did not compare or test production alternatives.
The decision in one table
The same question applies to each option: can a fresh worker inspect the original waiting thread after the first worker exits normally?
Local choice | Fresh-process inspection in our test | Choose it when | Do not infer |
|---|---|---|---|
InMemorySaver | No pending state in a fresh saver | A disposable, same-process test is sufficient | That the thread ID restores discarded RAM |
SqliteSaver with a database file | The original thread remained paused and could resume | A synchronous local prototype needs a process boundary | Crash tolerance, multi-worker failover or exactly-once provider writes |
SqliteSaver with :memory: | No pending state in a fresh connection | A disposable SQLite test is sufficient | That using a SQLite API makes the database durable |
The third row is a storage-mode control, not a third provider. We did not grade speed, scalability or security, and there is no universal winner. The deciding constraint here is whether losing local state at process exit is acceptable.
Want to evaluate the file-backed choice in an isolated environment? Start setup for the approval-storage reproduction, then use Code Interpreter Execute Code to run the bounded package below. Setup instructions are not a completed installation, and opening an Action page is not a successful execution.
For an agent that has not installed Aident, use the exact installation instruction:
Keep the intended job with the agent after setup:
If you already have a suitable local Python environment, the reproduction needs no Aident account. If you need to decide what the hosted execution surface does and does not isolate, read code interpreters versus agent sandboxes first. A hosted sandbox's lifetime is not a durable storage service.
What the first-hand test actually established
On October 3, 2026, we ran eight local scenarios with 48 assertions through Aident Loadout's Code Interpreter Tools. A separate fresh-reader run installed the exact 41 version pins below in a new virtual environment and passed pip check. The observed platform was Linux, Python 3.13.14, LangGraph 1.2.12, checkpoint 4.2.0, SQLite checkpoint 3.1.1 and LangChain Core 1.6.6.
The file-backed run used three separate processes. Start, inspect and approve had process IDs 1335, 1341 and 1342. Inspection found the same waiting approval without executing it. Resume then completed the dummy job. Fresh InMemorySaver and SQLite :memory: controls had no pending state. A wrong thread ID also had no pending state, while the original file-backed thread remained paused.
These are observations from synthetic fixtures, not prevalence, durability or performance statistics. Normal process exit is not a machine crash or disk-loss test. The worker processes shared a filesystem; different machines and isolated sandboxes do not automatically share that file.
LangGraph's persistence documentation distinguishes thread-scoped checkpoints from cross-thread stores. Picking a checkpointer for this wait is not a decision about long-term user memory.
Persistence does not stop approval-node replay
An approval wait also changes where work belongs. LangGraph's interrupt documentation explains the resume boundary: execution restarts the interrupted node, so code before interrupt() can run again.
In the file-backed fixture, a dummy effect placed before the interrupt in the same node occurred twice. Moving preparation to a prior completed node left its count at one in this normal-exit sequence. The approval node still ran twice. This does not eliminate every crash-window replay.
A separate local unique-key ledger admitted one effect in its tested sequential case. That is application-owned local idempotency, not an exactly-once guarantee for a provider API. Checkpoint storage does not prove that an email was sent, a refund happened or a payment was authorized.
Before replacing the dummy completion with a consequential Action, review risk approval versus spend approval. Storage, reviewer identity, exact approved inputs and spending authority are separate decisions.
Reproduce file-backed inspection and resume
Use a disposable directory and Python 3.13.14. The package installation contacts the package index; the graph itself uses only local files and SQLite, with no model or provider write. Do not point the demo at a production database or accept checkpoint files from untrusted sources.
Save the following as requirements.txt. These are tested version pins, not wheel hashes or supply-chain attestation.
Save this as approval_demo.py alongside it:
From that directory, create the isolated environment and run the three phases. Keep RUN_DIR unchanged:
The expected result, ignoring changing process IDs:
Command | Pending node | Prepared | Approval attempts | Dummy completions | Completed |
|---|---|---|---|---|---|
start | approval | 1 | 1 | 0 | false |
inspect, new process | approval | 1 | 1 | 0 | false |
approve, another process | none | 1 | 2 | 1 | true |
A successful reproduction contains all three phases, distinct process IDs and the complete state/counter trace, with no execution error. A successful setup or package install alone does not meet that condition.
The deny phase is available, but run it with a new paused job rather than reusing the completed one. The earlier denial scenario completed with no dummy completion. The current copyable start/inspect/approve sequence is not a new denial test.
Common mistakes after choosing SQLite
Changing the thread ID. Use the same thread_id for inspection and resume. A checkpoint is not found just because the database file exists.
Changing the database path. Separate relative working directories can create separate files. Keep the root unchanged across processes. A fresh sandbox or lost filesystem requires a different storage design.
Mistaking directory counters for thread state. This fixture's JSONL events count activity in the directory, not a per-thread production audit trail. Inspect state and next for the requested thread rather than attributing shared counters to it.
Starting over instead of inspecting. The demo rejects an existing job on start and rejects resume when no approval is pending. Do not substitute a replacement job and call it recovery.
Treating a boolean as authorization. The CLI decision is deliberately synthetic. It does not authenticate a reviewer or bind approval to an input hash.
Choosing production storage from this result. File-backed SQLite answered the local prototype question. Production concurrency, backups, failover, retention, encryption and external-write reconciliation need their own design and tests. AsyncSqliteSaver and PostgreSQL were not evaluated here.
Choose the smallest storage contract you actually need
For an expendable same-process experiment, keep the in-memory option. For a synchronous local approval demo that must survive an ordinary worker exit, use a file-backed SQLite checkpointer with the same database and thread identity. If the requirement is distributed or production recovery, stop at that boundary and evaluate production storage separately.
If you run the package through Loadout, retain its execution receipt and compare the full trace, not a reassuring completion message. Repeat the comparison when the checkpoint packages, interrupt semantics or your worker/storage boundary changes.
Sources and method
LangGraph checkpoint API reference: current saver scope and interfaces, checked October 3, 2026.
LangGraph persistence: checkpoint versus store scope.
LangGraph memory guidance: production guidance, not a tested alternative in this comparison.
LangGraph interrupts: interrupted-node restart behavior.
Original Code Interpreter receipts
log_2k96F5mcavyL(eight scenarios/48 assertions) andlog_1aq8lTRi31KL(fresh-reader package). Source SHA-256f5a695860334e8ddc028fa94b1edae4c73f6d70e7fcc8ae240ccaf547cb712cf; requirements SHA-256131933a01ef517cd31334cf09ec45bd9b4c15b7c9539b4ac8b4825a01e4f338b. The code above preserves those tested blocks.
About the author

Steve Wall
Steve Wall is an engineering and product leader with more than 20 years of experience in technology. He graduated from Carnegie Mellon University. At Aident, he covers technology and business trends, strategy, team building, resource allocation, and how organizations set and pursue high-level goals.



The one tool
for every tool
your agent needs.
Give any AI agent real capabilities in seconds. Connect 27,000+ tools once, skip the setup headache, and let your agents execute.


