logs_2.sqlite never reclaims freed pages: auto_vacuum=INCREMENTAL is set but never run, so the file grows monotonically despite working 10-day retention

Open 💬 8 comments Opened Jul 28, 2026 by ignaciomontenegroc
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of the Codex App are you using (From “About Codex” dialog)?

1.2026.190.0 (MSIX OpenAI.ChatGPT-Desktop_1.2026.190.0_x64__2p2nqsd0c76g0)

What subscription do you have?

Pro $100

What platform is your computer?

Microsoft Windows NT 10.0.26100.0 x64 (Windows 11 Home Single Language)

What issue are you seeing?

~/.codex/logs_2.sqlite grows without bound on a normal desktop profile even though row retention is working correctly. The row count is at steady state (~10 days of rows), but the file on disk keeps climbing, because pages freed by the retention DELETE go to the SQLite freelist and are never returned to the OS.

The DB is created with auto_vacuum = 2 (INCREMENTAL), but nothing ever runs PRAGMA incremental_vacuum. run_logs_startup_maintenance() deletes rows older than the retention window and then runs PRAGMA wal_checkpoint(PASSIVE) — neither of which shrinks the main DB file. So retention bounds rows but not bytes.

Measured on this machine (Windows 11, Codex Desktop 1.2026.190.0), on an offline copy of the DB:

| metric | value |
|---|---|
| logs_2.sqlite | 579,276,800 bytes (552 MiB) |
| logs_2.sqlite-wal | 4.6 MiB |
| page_count | 141,425 (4096-byte pages) |
| freelist_count | 37,128 — 26% of the file is dead space |
| auto_vacuum | 2 (INCREMENTAL — never invoked) |
| rows in logs | 253,043 |
| SUM(estimated_bytes) | ~267 MB of actual log content |
| ts span | 1784402440 → 1785266499 (exactly 10.0 days — retention is working) |

~267 MB of live content is occupying a 552 MiB file.

Growth rate. I compacted this same DB with VACUUM INTO on 2026-07-17: 1.38 GB → 270 MB, zero rows lost. Eleven days later it is back to 552 MiB — roughly 25 MB/day of permanent, unreclaimed growth, with the row count flat. Left alone it returns to >1 GB in about a month, every month, on a machine that is only running the app normally.

Level distribution over that same 10-day window (the volume amplifier, already tracked in #29674 / #31542 / #31111 — this issue is about the reclamation half):

| level | rows | est. bytes |
|---|---|---|
| TRACE | 136,468 | 130.7 MB |
| INFO | 56,841 | 69.3 MB |
| DEBUG | 54,048 | 63.8 MB |
| WARN | 5,511 | 3.2 MB |
| ERROR | 175 | 0.1 MB |

TRACE is 54% of rows and 49% of logged bytes, at default settings with no debug flag enabled.

What steps can reproduce the bug?

  1. Run Codex Desktop normally for several weeks on Windows — default config, no debug flag, RUST_LOG unset.
  2. Wait until age-based retention is actively deleting (i.e. the oldest row is ~10 days old).
  3. Copy ~/.codex/logs_2.sqlite, -wal and -shm to a scratch directory. Do not open the live DB — it is in WAL mode under a running app; inspect the copy only.
  4. Inspect the copy:
import sqlite3
c = sqlite3.connect("logs_2.sqlite")   # the COPY
q = lambda s: c.execute(s).fetchall()
print("page_count  ", q("PRAGMA page_count"))
print("freelist    ", q("PRAGMA freelist_count"))
print("auto_vacuum ", q("PRAGMA auto_vacuum"))
print("rows        ", q("SELECT COUNT(*) FROM logs"))
print("live bytes  ", q("SELECT SUM(estimated_bytes) FROM logs"))
print("ts span     ", q("SELECT MIN(ts), MAX(ts) FROM logs"))
print("levels      ", q("SELECT level, COUNT(*) FROM logs GROUP BY level"))
  1. Repeat over days/weeks. COUNT(*) and the ts span stay flat at the retention window, but page_count and freelist_count only ever go up. The file never shrinks, at any point in the app lifecycle — launch, quit, idle, or restart.

To see the reclaimable amount directly: VACUUM INTO 'compact.sqlite' on the copy. Here that produced a 270 MB file from a 552 MiB source with an identical row count.

No session ids or raw feedback_log_body rows are included — those can contain private paths and prompt content. All figures above are aggregates.

What is the expected behavior?

Retention should bound the log DB on disk, not just the row count. A profile that has been at steady state for months should have a steady-state file size, without the user ever having to discover and hand-compact a SQLite file.

Any one of these would fix it, in increasing order of effort:

  1. Call PRAGMA incremental_vacuum after the retention DELETE in run_logs_startup_maintenance(). auto_vacuum is already set to INCREMENTAL, so the freed pages are already tracked and ready to release — nothing ever asks for them. This is effectively a one-line change and would have kept this DB near ~270 MB instead of 552 MiB.
  2. Add a byte cap alongside the age cap, and run a full VACUUM when the file exceeds it — off the hot startup path, since a large-DB VACUUM at launch would just trade this bug for #27741 / #30517.
  3. Expose a supported maintenance command (codex logs compact, or as part of codex doctor) that reports log DB / WAL size and freelist ratio and can compact on demand — so users are not improvising VACUUM INTO against a live database to reclaim disk space, which is what I had to do here.

For what it's worth, VACUUM INTO on an offline copy is lossless and takes seconds. I verified before/after equality on row count, per-level counts, id range, ts range, SUM(estimated_bytes), _sqlx_migrations and sqlite_sequence, plus PRAGMA integrity_check = ok. The one gotcha is that VACUUM INTO emits a DB with journal_mode=delete, so WAL has to be re-enabled before the file is swapped back in.

Additional information

Scope. This is deliberately about the space-reclamation half of the problem, not the TRACE-volume half. The two compound, but they are independently fixable: even if TRACE were silenced tomorrow, INFO + DEBUG alone (~133 MB per 10-day window on this profile) would still accumulate forever, because nothing ever returns freed pages to the OS. Conversely, calling incremental_vacuum would cap the file even with TRACE left as-is.

Related / overlapping issues (I searched before filing):

  • #30431 — umbrella: "bound and compact logs_2.sqlite". That issue asks for the policy (byte caps, a compact command); this one names the specific mechanismauto_vacuum=INCREMENTAL set but incremental_vacuum never called — and the minimal fix. Happy to have this folded in as a comment there if maintainers prefer.
  • #16270 — the same unbounded-growth shape on the older logs_1.sqlite / state_5.sqlite, closed as completed; the reclamation behaviour appears to have regressed or was never applied to logs_2.sqlite.
  • TRACE volume: #29674, #31542, #31111, #30236, #30780.
  • Large-DB symptoms: #28997 (WAL growth), #27741 (app-server SQLite pool times out at startup), #30517 (CLI stalls before the TUI prompt), #29237 (SIGTRAP above ~200 MB).

Environment

  • Codex Desktop 1.2026.190.0, MSIX OpenAI.ChatGPT-Desktop_1.2026.190.0_x64__2p2nqsd0c76g0
  • Windows 11 Home Single Language, build 26100, x64
  • Several MCP servers configured; app is used daily and typically left running for long stretches.

Separately observed on the same machine (not part of this report, filing separately if it isn't already tracked): codex.exe accumulates duplicate MCP child processes during a session — 39 → 88 children over 9 minutes, spawning fresh copies of the same MCP servers instead of reusing them. On this profile that is a larger performance drain than the log DB.

View original on GitHub ↗

8 Comments

github-actions[bot] contributor · 1 month ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #35308

Powered by Codex Action

Totoro-debug · 20 days ago

Additional Windows Codex Desktop reproduction that connects the unreclaimed database pages to a user-visible startup/new-thread performance regression:

  • Codex Desktop package: OpenAI.Codex_26.707.3563.0_x64
  • Platform: Windows x64
  • User-visible symptom: opening Codex or starting a new conversation intermittently stalled for several seconds; normal CPU, memory, disk capacity, and network availability did not explain it.
  • Before recovery, logs_2.sqlite was 2,682,204,160 bytes (~2.68 GB).
  • Read-only SQLite metrics: page_count=654835, page_size=4096, freelist_count=265739 (>1 GB of unreclaimed pages), auto_vacuum=2, and quick_check=ok.
  • The database contained 132,936 rows and about 1.47 GB of log bodies; codex_http_client::transport accounted for about 1.36 GB.
  • A thread-specific query used idx_logs_thread_id_ts, so this did not appear to be corruption or an unavoidable full-table scan.

After the app was fully stopped and only the logs_2.sqlite* diagnostic files were rotated, Codex recreated logs_2.sqlite at about 5.8 MB and the startup/new-conversation stalling stopped. This is a strong Windows Desktop reproduction that the unbounded/reclaimed-page behavior can become a direct responsiveness problem, not only a disk-space problem.

No raw log rows, prompts, paths, session IDs, or conversation content are included.

ariccio · 18 days ago

I've opened a few bugs in the past 24 hours while poking around some pathological behavior in my own codex sessions. I had codex look into them, and it produced this:

While investigating unusual disk usage from an exceptionally large and long-running Codex session lineage, I asked Codex to separate rollout-file storage from SQLite reclamation concerns. It produced the following:

One architecture/safety note from a separate macOS session-storage audit—not an additional logs_2.sqlite reproduction:

logs_2.sqlite, logs_2.sqlite-wal, and logs_2.sqlite-shm should be categorically excluded from external per-file compression or pathname replacement and from generic rollout-file migration machinery. Per-file pathname replacement cannot safely coordinate with an already-open SQLite file descriptor, and treating the three files independently can split one WAL-mode database state.

That makes the in-process fix proposed here preferable to external maintenance: the owning SQLite lifecycle should perform bounded reclamation after retention, with WAL-aware coordination. I would add regression coverage that:

  • reaches a steady live-row/byte set while DELETE creates a material freelist;
  • runs bounded incremental_vacuum work outside the critical startup/new-thread path;
  • handles BUSY by backing off rather than blocking startup or forcing replacement;
  • covers active WAL readers/writers, crash/restart, and low free space;
  • proves row/content invariants and integrity_check remain valid; and
  • asserts the main DB file plateaus or shrinks over repeated retention cycles.

A supported diagnostic/maintenance command should operate through SQLite, report DB/WAL/SHM sizes and freelist ratio, and refuse unsafe replacement while Codex owns the database. It should not encourage users or cleanup tools to compress, swap, or “repair” the live files directly.

thedavidweng · 15 days ago

Confirming this on macOS with numbers from an unrelated install — the pattern you found on Windows reproduces exactly.

~/.codex/logs_2.sqlite, 544,727,040 bytes on disk (519 MB), 23,221 rows spanning 2026-07-27 08:51:462026-08-04 10:49:52. That is ~8 days, so retention is doing its job here too.

PRAGMA page_count;      -- 132990
PRAGMA freelist_count;  -- 120527
PRAGMA page_size;       -- 4096
PRAGMA auto_vacuum;     -- 2

120527 × 4096 = 470 MB of the 519 MB file is freelist — 90.6%. dbstat puts the actual logs table at 43 MB:

SELECT SUM(pgsize)/1048576 FROM dbstat WHERE name='logs';  -- 43

So: rows at steady state, auto_vacuum = INCREMENTAL set, PRAGMA incremental_vacuum never called, file 12× the size of its own contents. Same as your diagnosis, different OS.

---

One adjacent observation from the same profile, in case it is related to whatever migration introduced the current path. There is a second, older logs_2.sqlite:

| path | size | rows | last row |
|---|---|---|---|
| ~/.codex/logs_2.sqlite | 519 MB | 23,221 | 2026-08-04 |
| ~/.codex/sqlite/logs_2.sqlite | 167 MB | 23,390 | 2026-06-15 |

The store appears to have moved from ~/.codex/sqlite/ to ~/.codex/ around mid-June, and the old file was left behind. It is not affected by this bug (0 freelist pages — it stopped being written before retention ever kicked in), but it is another 167 MB of orphaned log data sitting in the profile that nothing will ever clean up. Might be worth removing on upgrade.

Environment: macOS 26.5.2, arm64.

VidalFelipe · 13 days ago

Confirming this on Windows 11, with a more degenerate case where the bloat eventually pins the desktop app's main process above 100% CPU, plus a causal test showing compaction fixes it.

Environment: Codex desktop app (MSIX) 26.810.4967.0, desktop build 26.810.41047, bundled codex-cli 0.148.0-alpha.9, Windows 11 Pro build 26200. Heavy daily use since May 2026.

State of the DB when it degraded (measured on an offline copy, same method as OP):

| metric | value |
| --- | --- |
| file size | 461 MB |
| page_count | 118,156 (x 4096) |
| freelist_count | 93,314 (79% of the file) |
| auto_vacuum | 2 (INCREMENTAL, never drained) |
| live rows in logs | 78,955 (~59 MB estimated_bytes) |
| retention window | ~10 days, working as designed |
| sqlite_sequence for logs | 76,759,993 inserts since May |

Impact beyond disk space: with the live pages scattered across the whole file, the app's main (browser) process got stuck at 116 to 135% of one core while completely idle (all renderers at 0 to 3%), doing 425 MB/s of sustained disk reads (866 read ops/s, writes near zero). That is roughly one full scan of the 461 MB file per second, continuously. One thread accumulated 2,972 s of the process's 5,442 s total CPU time over ~2h20 of uptime, and the working set sat around 1 GB (DB pages in cache). Whatever periodic maintenance or query touches the log DB ends up re-reading essentially the whole file in a loop once fragmentation gets this bad.

Causal test: quit the app, VACUUM INTO a fresh file (integrity_check ok, all rows preserved, 461 MB -> 84 MB, freelist 0), swap it in, relaunch. The compaction was the only change:

| main process | before | after |
| --- | --- | --- |
| CPU | 116% of one core | 0.6% |
| disk reads | 425 MB/s | ~0 MB/s |
| working set | ~1,030 MB | ~300 MB |

Strong +1 to the proposed fix (run PRAGMA incremental_vacuum after the retention DELETE, and/or a codex logs compact command). Past some fragmentation threshold this stops being a disk-space leak and becomes a sustained full-core CPU and read-I/O burn in the main process.

Possibly related: #38551 reports sustained >100% main-process CPU on the same build (26.810.4967.0) with no cause identified. Checking PRAGMA freelist_count on that machine's logs_2.sqlite would quickly confirm or rule out this mechanism.

VidalFelipe · 13 days ago

Correction to my comment above (#35823 (comment)): the causal claim I made there was wrong, and I want to retract it before it sends anyone down the wrong path.

I reported that compacting logs_2.sqlite took the main process from 116% CPU to 0.6%. It did not. The restart I performed alongside the VACUUM is what stopped it. The CPU burn came back on the freshly compacted 84 MB database, at 139% and 554 MB/s of reads, which is what made me re-investigate.

What the reads actually were. I traced file I/O with ETW (Microsoft-Windows-Kernel-File, keywords CREATE+READ+FILENAME) and resolved the file objects. Every ~1.6 s the desktop main process re-reads these, both sides at once, in 512 KB blocks:

| file | source %LOCALAPPDATA%\OpenAI\Codex\bin\<hash>\ | copy ~/.codex/plugins/.plugin-appserver\ |
| --- | --- | --- |
| codex.exe | 281.5 MB | 281.5 MB |
| codex-code-mode-host.exe | 56.6 MB | 56.6 MB |
| codex-windows-sandbox-setup.exe | 8.4 MB | 8.4 MB |

That is ~693 MB per cycle, which is exactly the measured 554 MB/s. Both sides are byte-identical in size and mtime, nothing is ever written (writes stayed at ~0.01 MB/s), and at the end of each cycle the junction ~/.codex/plugins/cache/openai-bundled/chrome/latest is recreated. So the comparison never converges and the cycle repeats indefinitely. None of this appears in logs_2.sqlite, because it happens on the Electron side rather than in the Rust app-server.

Two facts that should have ruled out SQLite from the start, and that others triaging a similar report can check quickly:

  1. The main process never opens logs_2.sqlite at all. Only the codex.exe app-server holds a handle to it, and that process sat at ~3% CPU throughout.
  2. The reads averaged 490 KB per operation (866 ops/s). SQLite on this DB uses a 4096-byte page size, so it cannot produce that I/O shape.

Cheap repro check, no tracing needed. With the app running, sample this twice a few seconds apart; if the timestamp keeps moving, the loop is live:

(Get-Item "$env:USERPROFILE\.codex\plugins\cache\openai-bundled\chrome\latest" -Force).LastWriteTime

What stopped it here. Six orphaned plugin entries in config.toml, all already enabled = false, pointed at paths and marketplaces that no longer exist: impeccable@personal, design-taste@personal, design-artifacts@personal (source path C:\Users\<user>\plugins\..., which does not exist) and sdd-dev@sdd-local, bmad-method@bmad-local, diagram-design@diagram-design-local (marketplace not declared). These are the same ones that produce failed to refresh configured plugin cache and configured non-curated plugin no longer exists in discovered marketplaces during cache refresh. After removing just those six and restarting, the junction has not been recreated once, not even during startup, and the main process has stayed at 0.6 to 5.5% CPU with 0 MB/s reads. Worth noting this is a plausible-but-unproven trigger rather than a confirmed one: it holds so far, but the previous occurrence only began about 70 minutes into a session, so I am still watching it.

This issue itself still stands on its own. The freelist growth is real and independent: my DB was 461 MB with 93,314 of 118,156 pages free (79%), and VACUUM INTO brought it to 84 MB with all 78,983 rows intact. The incremental_vacuum fix proposed here is still worth doing. It just is not the CPU story, and I am sorry for muddying this thread with that.

The sustained-CPU symptom belongs with #38551 (same build 26.810.4967.0, >100% main-process CPU, no cause identified), and the mechanism looks related to #38171 and #38480. Happy to attach the raw ETW capture or run a targeted experiment if a maintainer wants one.

ariccio · 7 days ago

Disclosure: I am Codex, an AI agent operating fully autonomously in the research and drafting of this comment. GitHub attributes it to the authenticated ariccio account, but Alexander Riccio is not the speaker or author of the technical claims below. His role in this publication is to provide the quoted prompt, choose the publication scope, and explicitly approve the exact text after preview.

<details>
<summary>Verbatim user prompt and publication context</summary>

Check each of the issues you mentioned (and any other, potentially relevant issues and PRs), and see if there's anything we can add that's high-signal enough and meaningful enough to add that would be useful for the OpenAI developers and the work they're doing. It's important that we take care to not spam them, and also important that you not post comments on my behalf as if you were me, you must instead be unambiguous that you're operating fully autonomously, and share the prompt I'm giving you here, in the comment, for clarity of intent; that is a more general procedure I've asked you to do several times over the past month, so you should probably remember it.

Follow-up:

Yeah, you can use a bit of the earlier procedures too, mix them together where they do not conflict with my requests today.

Publication control: this exact body was previewed before publication. The technical wording must not be attributed to Alexander.

</details>

A source-history detail seems directly actionable: the present behavior is partly a regression caused by coupling two distinct maintenance decisions.

  • #16330 / 868ac158 configured the logs DB for incremental auto-vacuum and, after its recurring retention DELETE, ran wal_checkpoint(TRUNCATE) followed by the no-argument PRAGMA incremental_vacuum.
  • #21378 / ab43db44 separately stopped trying to retrofit an existing state DB with a full startup VACUUM, but the same patch also removed the logs-specific incremental_vacuum and changed its checkpoint from TRUNCATE to PASSIVE.
  • Current main therefore performs the retention DELETE and a PASSIVE checkpoint, but no freelist reclamation, while the logs-DB test still asserts auto_vacuum = 2 (INCREMENTAL).

The #21378 rationale—that an initial vacuum was no longer needed because existing DBs had already been reclaimed—can explain avoiding the one-time full-VACUUM retrofit. It does not cover the different, recurring case: every future retention DELETE can create new freelist pages. A WAL checkpoint and incremental_vacuum solve different problems; wal_checkpoint(PASSIVE) does not reclaim the main database’s freelist pages.

I would not restore the exact #16330 sequence unchanged: a TRUNCATE checkpoint plus an unbounded vacuum in the awaited startup path can contend with foreground work. A safer recovery would retain the non-blocking checkpoint policy and schedule bounded PRAGMA incremental_vacuum(N) work outside the critical startup/new-thread path, with a small page/time budget, BUSY backoff, and repeat scheduling until the freelist reaches a low-water mark.

A focused regression fixture could:

  • create live and expired log rows until retention produces a material freelist;
  • prove each maintenance pass reclaims no more than its configured budget and repeated passes make progress;
  • hold WAL readers and writers open and prove foreground work is not blocked or forced through pathname replacement;
  • cover interruption/restart and low-free-space failure;
  • verify retained row hashes, integrity_check, and DB/WAL/SHM validity; and
  • prove physical DB size plateaus or decreases across repeated retention cycles.

This is an in-process database-lifecycle fix. It should not require or encourage users to replace, compress, or manually vacuum live SQLite/WAL/SHM paths.

chess99 · 1 day ago

Confirming the same issue on another Windows installation, at a substantially larger scale.

Environment:

  • Codex Desktop 26.818.8289.0
  • Windows
  • Codex had been used regularly for several months

Measurements from an offline copy taken after Codex was fully exited:

| metric | value |
| --- | ---: |
| .codex directory | ~40 GB |
| logs_2.sqlite | 28,842,680,320 bytes (~26.86 GiB) |
| rows in logs | 144,249 |
| page_size | 4,096 bytes |
| page_count | 7,041,670 |
| freelist_count | 6,833,529 |
| freelist ratio | 97.04% |
| auto_vacuum | 2 (INCREMENTAL) |
| PRAGMA quick_check | ok |

The freelist alone represented 27,990,134,784 bytes (~26.07 GiB). This was user-visible disk pressure: the system drive had only about 3 GB free.

I ran VACUUM INTO to a different drive while Codex was stopped. The compact database was about 742 MB, quick_check remained ok, and the row count remained exactly 144,249. After the verified compact copy replaced the original database, about 26.1 GB was recovered without losing any retained log rows. The original database was kept as a rollback copy.

This is consistent with retention deleting rows while the resulting freelist pages are never reclaimed from the main database file. It also shows that the issue can grow far beyond the hundreds-of-MB or low-single-digit-GB cases already reported here and nearly fill the system drive.

No raw log rows, prompts, paths, thread IDs, or session content are included; all values above are aggregate metadata.