InMemorySaver vs SqliteSaver: Which Survives an Approval Wait?

InMemorySaver vs SqliteSaver: Which Survives an Approval Wait?

Steve Wall

A translucent coral sweep fades beside a luminous citron seed held in a broad lilac bowl on cobalt, contrasting transient and retained state.

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:

Follow https://aident.ai/SETUP.md

Keep the intended job with the agent after setup:

Use Aident Loadout to discover Code Interpreter Execute Code and inspect
its current schema, Vault readiness and exact-input preflight. Reproduce
only the local dummy approval-storage package in this article. Use a
fresh disposable environment, keep one state directory and thread ID,
and run start, inspect and approve as separate Python processes.
Do not call a model or any consequential external Action. Do not upload
a production checkpoint database or customer data. Stop if the runtime
or packages differ from the documented test, installation fails, or a
preflight requires additional approval. Return the execution receipt,
runtime/package versions, distinct process IDs and the expected trace.
Do not describe this as crash safety or real human authorization

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.

aiosqlite==0.22.1
annotated-types==0.8.0
anyio==4.15.1
certifi==2026.7.22
charset-normalizer==3.5.2
distro==1.9.0
h11==0.16.0
httpcore==1.0.9
httpcore2==2.13.1
httpx==0.28.1
httpx2==2.13.1
idna==3.20
jsonpatch==1.33
jsonpointer==3.1.1
langchain-core==1.6.6
langchain-protocol==0.0.19
langgraph==1.2.12
langgraph-checkpoint==4.2.0
langgraph-checkpoint-sqlite==3.1.1
langgraph-prebuilt==1.1.0
langgraph-sdk==0.4.5
langsmith==0.14.4
orjson==3.12.0
ormsgpack==1.12.2
packaging==26.3
pydantic==2.13.5
pydantic_core==2.46.5
PyYAML==6.0.3
requests==2.34.2
requests-toolbelt==1.0.0
sniffio==1.3.1
sqlite-vec==0.1.9
tenacity==9.1.4
truststore==0.10.4
typing-inspection==0.4.4
typing_extensions==4.16.0
urllib3==2.8.0
uuid_utils==0.17.1
websockets==16.1.1
xxhash==4.0.1
zstandard==0.25.0

Save this as approval_demo.py alongside it:

import argparse
import json
import os
import pathlib

os.environ["LANGGRAPH_STRICT_MSGPACK"] = "true"
os.environ["LANGSMITH_TRACING"] = "false"

from typing_extensions import TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.types import interrupt, Command
from langgraph.checkpoint.sqlite import SqliteSaver

parser = argparse.ArgumentParser()
parser.add_argument("phase", choices=["start", "inspect", "approve", "deny"])
parser.add_argument("--root", required=True)
parser.add_argument("--thread", default="dummy-thread")
args = parser.parse_args()
root = pathlib.Path(args.root)
root.mkdir(parents=True, exist_ok=True)
events_file = root / "events.jsonl"

def event(kind):
    with events_file.open("a") as handle:
        handle.write(json.dumps({"kind": kind, "pid": os.getpid()}) + "\n")

class State(TypedDict):
    job: str
    approved: bool
    completed: bool

def prepare(state):
    event("prepared")
    return {}

def approval(state):
    event("approval_attempt")
    decision = interrupt({"job": state["job"], "question": "Approve local dummy completion?"})
    if type(decision) is not bool:
        raise ValueError("This fixture accepts only a boolean decision")
    return {"approved": decision}

def finish(state):
    if state["approved"]:
        event("dummy_completion")
    return {"completed": state["approved"]}

builder = StateGraph(State)
builder.add_node("prepare", prepare)
builder.add_node("approval", approval)
builder.add_node("finish", finish)
builder.add_edge(START, "prepare")
builder.add_edge("prepare", "approval")
builder.add_edge("approval", "finish")
builder.add_edge("finish", END)

with SqliteSaver.from_conn_string(str(root / "checkpoints.sqlite")) as saver:
    graph = builder.compile(checkpointer=saver)
    config = {"configurable": {"thread_id": args.thread}}
    before = graph.get_state(config)
    if args.phase == "start":
        if before.values:
            raise ValueError("Use a fresh root/thread; do not restart an existing job")
        graph.invoke({"job": "dummy-job", "approved": False, "completed": False}, config)
    elif args.phase in {"approve", "deny"}:
        if before.next != ("approval",):
            raise ValueError("No pending approval for this root/thread; inspect first")
        graph.invoke(Command(resume=args.phase == "approve"), config)
    after = graph.get_state(config)
    events = [json.loads(line) for line in events_file.read_text().splitlines()] if events_file.exists() else []
    counts = {kind: sum(row["kind"] == kind for row in events)
              for kind in ["prepared", "approval_attempt", "dummy_completion"]}
    print(json.dumps({"phase": args.phase, "pid": os.getpid(), "state": after.values,
                      "next": after.next, "counts": counts}))

From that directory, create the isolated environment and run the three phases. Keep RUN_DIR unchanged:

python3.13 -m venv .venv
.venv/bin/python -m pip install -r requirements.txt
.venv/bin/python -m pip check
RUN_DIR="$(mktemp -d)"
.venv/bin/python approval_demo.py start --root "$RUN_DIR"
.venv/bin/python approval_demo.py inspect --root "$RUN_DIR"
.venv/bin/python approval_demo.py approve --root "$RUN_DIR"

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) and log_1aq8lTRi31KL (fresh-reader package). Source SHA-256 f5a695860334e8ddc028fa94b1edae4c73f6d70e7fcc8ae240ccaf547cb712cf; requirements SHA-256 131933a01ef517cd31334cf09ec45bd9b4c15b7c9539b4ac8b4825a01e4f338b. The code above preserves those tested blocks.

About the author

Illustrated editorial avatar of Steve Wall

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.

Home

Home

Home

Integrations

Integrations

Integrations

Vault

Vault

Vault

Audit

Audit

Audit

Arana Grande

Arana Grande

Arana Grande

Free

Free

Free

30-day audit summary

30-day audit summary

30-day audit summary

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

Daily action-call volume and the latest receipts from the Loadout audit trail.

View Audit

View Audit

View Audit

Loadout usage

Loadout usage

Loadout usage

617 action calls in the last 30 days

617 action calls in the last 30 days

617 action calls in the last 30 days

May 19 - Jun 17

May 19 - Jun 17

May 19 - Jun 17

10 active days

10 active days

10 active days

Less

Less

Less

More

More

More

Recent activity

Recent activity

Recent activity

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Latest action-call receipts from connected agents

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Apr 23, 09:23 AM

Shopify

Shopify

Shopify

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Creates Or Updates An Asset For A Theme

Success

Success

Success

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Apr 23, 09:21 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Success

Success

Success

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Apr 23, 08:53 AM

Shopify

Shopify

Shopify

Update Products Param Product Id

Update Products Param Product Id

Update Products Param Product Id

Failed

Failed

Failed

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Apr 22, 22:13 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Apr 22, 22:12 PM

Shopify

Shopify

Shopify

Create Product Image

Create Product Image

Create Product Image

Success

Success

Success

Connected integration coverage

Connected integration coverage

Connected integration coverage

162

162

162

of 753 accessible connected

of 753 accessible connected

of 753 accessible connected

Callable actions

Callable actions

Callable actions

1,126

1,126

1,126

Vault credentials

Vault credentials

Vault credentials

8

8

8

Explore what's possible

Explore what's possible

Explore what's possible

See all Integrations

See all Integrations

See all Integrations

Google Ads

Google Ads

Google Ads

All available Goolge Ads tools via...

All available Goolge Ads tools via...

All available Goolge Ads tools via...

X (twitter)

X (twitter)

X (twitter)

All available X tools via...

All available X tools via...

All available X tools via...

Github

Github

Github

All available Github tools via...

All available Github tools via...

All available Github tools via...

Notion

Notion

Notion

All available Notion tools via...

All available Notion tools via...

All available Notion tools via...

Slack

Slack

Slack

All available Slack tools via...

All available Slack tools via...

All available Slack tools via...

Firecrawl

Firecrawl

Firecrawl

All available Firecrawl tools via...

All available Firecrawl tools via...

All available Firecrawl tools via...

753 integrations are available for loadouts.

753 integrations are available for loadouts.

753 integrations are available for loadouts.

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.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.

Try Aident Loadout

Empower your Codex or OpenClaws to get real jobs done. Connect 27,000+ tools in one prompt, and let your agents deliver real results.