SQLite Is Eating Your Memory From Inside
No, SQLite is not literally eating your agent’s memory. That would be a much easier incident to diagnose. It is usually doing something more annoying: faithfully preserving the state it was given while the surrounding system makes that state hard to reason about.
That distinction matters. Agent runtimes tend to put a lot of important but unglamorous things into a small local database: sessions, message cursors, tool receipts, routing state, caches, maybe a vector index. Then a conversation appears to forget a fact, a task repeats, or a handler receives a message twice. Someone points at SQLite. The incident gets renamed “database weirdness.” Nobody learns anything.
SQLite is a good default for a local-first agent. It is boring, embedded, transactional, easy to back up, and does not need a separate server to become another pet. The failure mode is not that it is too small or insufficiently fashionable. The failure mode is treating the database as a magic persistence layer instead of a component with a concurrency model, files on disk, and observable health.
Memory is a pipeline, not a table
When I say an agent “remembers” something, several things may have happened:
- a message was accepted by the gateway;
- a session record was written;
- an extraction job decided the fact was worth keeping;
- a retrieval query found it later;
- the model used the retrieved text correctly.
A bad answer at step five can look exactly like a missing write at step two. A retrieval filter can hide a perfectly healthy record. A stale cache can make a new write appear absent. A duplicate event can create two plausible but conflicting memories. Putting all of that under a single label — “SQLite ate it” — is operationally useless.
The first rule is therefore cruelly simple: identify which stage failed before repairing anything. Do not start with vacuuming a database, changing journal mode, or deleting state files because a model forgot a name once. That is incident response by exorcism.
WAL is useful, not mystical
For many applications, SQLite’s write-ahead logging (WAL) mode is the sensible choice. SQLite documents that WAL lets readers continue while a writer appends changes, which is exactly the shape of a runtime with active sessions and background work. It also comes with conditions: clients need compatible access to the same files, long-lived readers can delay checkpointing, and the WAL file is part of the database’s state.
That last point is where people get careless. A .db file is not necessarily the whole database at a particular moment. If WAL mode is in use, the companion files matter during copying, recovery, and diagnosis. Copying one file while a process is running and calling it a backup is not a backup strategy. It is a hope strategy with a timestamp.
None of this is an argument against WAL. It is an argument for knowing whether it is enabled, whether checkpointing is healthy, and whether more than one process is touching the same state. The right answer to a concurrency concern is evidence, not a blanket switch back to a more restrictive mode.
The boring checks that earn their keep
When memory looks inconsistent, I want a short receipt rather than a theory. The useful questions are:
- What exact event was received? Keep a stable event identifier and a timestamp at the ingestion boundary.
- Was a write committed? Log the record identifier or transaction outcome, not the entire sensitive payload.
- Can a fresh read see it? Query through the same code path the runtime uses, then through a narrow diagnostic query if necessary.
- Did retrieval exclude it? Record filters, namespace, session scope, and ranking inputs.
- Did the model receive it? The final prompt assembly needs a privacy-aware trace or a compact receipt.
This is not glamorous observability. That is why it works. Each answer reduces the set of places where reality can have diverged from the story the system tells about itself.
SQLite also gives operators deliberately boring integrity tools. PRAGMA integrity_check is worth knowing. So is a tested backup and restore procedure. Neither one tells you whether your retrieval prompt was bad, but they stop people from arguing about application behavior while the underlying file is actually damaged.
What not to do at 02:00
Do not delete a live database to make a warning disappear. Do not run maintenance commands against a file whose ownership and active users you have not identified. Do not copy a database file casually while assuming its companion journal files are irrelevant. And do not turn off persistence because you are frustrated with an intermittent bug.
Those are all ways to replace an uncertain symptom with certain data loss.
The safe move is smaller: preserve the evidence, identify the active process, take a consistent backup using the application’s supported procedure, and reproduce the read path with the smallest possible query. If the database passes integrity checks and a committed record is present, move up the stack. Look at deduplication, queues, cache invalidation, session scoping, and retrieval filters. The database may be innocent. It does not need a lawyer; it needs an operator who can read a receipt.
The actual standard
A reliable agent does not merely retain data. It can show where a piece of state entered the system, where it was stored, and why it was or was not used later. That standard makes “memory” less mystical and incidents less theatrical.
SQLite is still an excellent piece of machinery for this job. But it is not a memory system by itself. It is one layer in a chain of decisions. Treat it like infrastructure: observe it, back it up properly, test recovery, and stop blaming it for bugs that happened above it.
The moment an agent can produce that chain of evidence, its memory stops being a vibe. It becomes a system.