Skip to content

The WAL never resets while long-lived sessions hold the store open #40

Description

@jakewan

Context

Surfaced while examining whether the store is bounded. The observation table turns out not to be where growth happens.

Problem

The store runs in WAL mode and takes concurrent writes from several long-lived sessions, which CLAUDE.md names as the normal case rather than an edge. Those two facts interact in a way nothing in the design accounts for: a passive checkpoint cannot reset the WAL while a reader still holds a snapshot needing pages in it, and a long-lived agent session is exactly such a reader.

So the WAL grows and never resets. Nothing in the DSN sets wal_autocheckpoint and no code path issues an explicit checkpoint, which leaves SQLite's default as the entire strategy — and that default cannot succeed under the concurrency pattern the project declares normal.

Consequences, in rough order of how soon they matter: the file grows without bound for as long as any session stays open; crash-recovery time scales with WAL size; and SQLite documents that read performance degrades as a WAL grows large, though that is not measured here.

Note this is a working-cost problem, not a data-volume one. The observation table is 240 KB after roughly 270 records and could grow for years without mattering. Permanence is the design; the WAL is not.

Evidence

Measured on one machine, 2026-08-21:

  • field-docket.db — 245,760 bytes, last modified 2026-08-17 22:32.
  • field-docket.db-wal — 4,136,512 bytes: 17× the database, holding four days of writes never merged back.
  • Page size is 4096, so a WAL frame is 4120 bytes. (4136512 - 32) / 4120 is exactly 1004 frames — just past SQLite's default wal_autocheckpoint of 1000 pages, which is the shape of an autocheckpoint firing on commit and failing to reset.
  • Four field-docket processes hold the database open, each on two descriptors (the separate read and write handles). The oldest started 2026-08-17 10:56 — before the database file's own last write — and had been alive just under four days when measured.

Suggested approaches

Genuinely uncertain, and the concurrency invariant constrains all of them.

  • Checkpoint as a session closes. An exiting session is the one moment it holds no reader snapshot, so a TRUNCATE checkpoint there would reset the WAL whenever the last session leaves. Does nothing while sessions overlap continuously, which may be the common case on an active machine.
  • Set wal_autocheckpoint deliberately rather than inheriting the default, making the current behaviour an explicit choice rather than an incidental one — even if it still often fails to reset.
  • Bound it out of band. field-docket snapshot already consolidates through VACUUM INTO, so an operator-facing checkpoint subcommand would fit an established precedent.
  • Accept and document it. SECURITY.md § Durability already discusses WAL trade-offs and does not mention this one. If the read-cost curve is flat enough to ignore, saying plainly that the WAL grows while sessions are open may be the whole fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions