Excessive SQLite WAL writes during streaming due to TRACE logs ignoring RUST_LOG

Open 💬 18 comments Opened Apr 10, 2026 by piotrkacala

What version of the IDE extension are you using?

openai.chatgpt-26.406.31014-linux-x64 (VSCodium on Linux Mint)

What subscription do you have?

ChatGPT Plus

Which IDE are you using?

VSCodium (Code - OSS fork)

What platform is your computer?

Linux x64 (Linux Mint)

What issue are you seeing?

During model streaming responses, the Codex app-server binary writes ~5 MiB/s (observed up to ~16 MiB/s in iotop) to ~/.codex/logs_2.sqlite-wal. This results in sustained disk write rates that can significantly impact SSD longevity and system performance.

This issue is independent of the thread-stream-state-changed warning spam and the open-in-targets retry loop, which were investigated separately.

Root cause

The extension correctly sets RUST_LOG=warn when spawning the app-server process — confirmed via /proc/<pid>/environ:

RUST_LOG=warn

However, the SQLite logging sink appears to bypass or not respect the RUST_LOG filter, since TRACE-level entries are still persisted to logs_2.sqlite despite RUST_LOG=warn. The SQLite logging path appears to operate outside of the standard RUST_LOG filtering mechanism, or uses a separate filter configuration.

This was confirmed via strace during streaming:

pwrite64(20</home/k/.codex/logs_2.sqlite-wal>, ..., 4096, ...) = 4096
pwrite64(20</home/k/.codex/logs_2.sqlite-wal>, ..., 4096, ...) = 4096
...

And via SQLite query — MAX(id) jumped by ~5000 rows for a single short (~50 token) response, while total row count remained stable, indicating that rows are being rapidly inserted and pruned/rotated during streaming:

-- Before response:
SELECT MAX(id), COUNT(*) FROM logs;  -->  3088667 | 30141

-- After response:
SELECT MAX(id), COUNT(*) FROM logs;  -->  3093673 | 30141

Log level breakdown in logs_2.sqlite:

TRACE  20485  (68%)
INFO    8286
DEBUG   1326
WARN      44

Sample TRACE entries written during streaming:

target=hyper_util::client::legacy::pool   feedback_log_body="idle interval checking for expired"
target=log                                feedback_log_body="WouldBlock"
target=log                                feedback_log_body="/home/runner/work/codex/codex/.cargo-home/git/checkouts/tokio-tungstenite-..."

These are low-level internal network events from tokio-tungstenite and hyper_util with no diagnostic value for end users. They should not be persisted to disk in a production binary — and critically, they are not suppressed by RUST_LOG=warn as one would expect.

What steps can reproduce the bug?

  1. Open a workspace in VSCodium/VS Code with the Codex extension.
  2. Confirm RUST_LOG=warn is set:
cat /proc/$(pgrep -f 'codex.*app-server')/environ | tr '\0' '\n' | grep RUST_LOG
  1. Run strace -f -e trace=pwrite64 -p $(pgrep -f 'codex.*app-server') in a separate terminal.
  2. Ask any question in the Codex chat panel and observe streaming.
  3. Observe continuous pwrite64 calls to logs_2.sqlite-wal at ~5 MiB/s despite RUST_LOG=warn.

Alternatively, check log level distribution after a session:

sqlite3 ~/.codex/logs_2.sqlite "SELECT level, COUNT(*) FROM logs GROUP BY level ORDER BY COUNT(*) DESC"

What is the expected behavior?

RUST_LOG=warn should suppress TRACE and DEBUG output everywhere, including the SQLite logging sink. The current behavior — where RUST_LOG=warn suppresses stderr output but does not affect SQLite persistence — is inconsistent and undocumented.

TRACE-level events from internal networking libraries (tokio-tungstenite, hyper_util) should never be written to the persistent SQLite log database during normal operation.

Additional information

Suggested fix

The SQLite logging sink in app-server should apply the same log-level filter as RUST_LOG (or the internal tracing subscriber filter), rather than maintaining a separate or hardcoded verbosity level. The fix should ensure that setting RUST_LOG=warn — as the extension already does — is sufficient to suppress TRACE/DEBUG writes to logs_2.sqlite.

Workaround

Users can redirect logs_2.sqlite to tmpfs to protect their SSD while waiting for a fix:

# stop VSCodium first
mv ~/.codex/logs_2.sqlite ~/.codex/logs_2.sqlite.bak
ln -s /tmp/logs_2.sqlite ~/.codex/logs_2.sqlite

Note: logs_2.sqlite contains only internal diagnostic logs — no conversation history — so data loss on reboot is acceptable.

After symlinking logs_2.sqlite to tmpfs, disk I/O from the Codex extension dropped to zero during streaming, confirming that the SQLite WAL is the sole source of the observed write load.

Workaround 2: block inserts into the SQLite log table

Another workaround was suggested by @synap5e in this comment:
https://github.com/openai/codex/issues/17320#issuecomment-4514992998

Instead of redirecting logs_2.sqlite to tmpfs, users can prevent new rows from being inserted into the logs table by adding a SQLite trigger:

sqlite3 ~/.codex/logs_2.sqlite "CREATE TRIGGER IF NOT EXISTS block_log_inserts BEFORE INSERT ON logs BEGIN SELECT RAISE(IGNORE); END;"

This causes attempted inserts into the logs table to be ignored without returning an error to the application, which should prevent logs_2.sqlite-wal from growing due to persistent TRACE/DEBUG log writes.

To remove the workaround:

sqlite3 ~/.codex/logs_2.sqlite "DROP TRIGGER IF EXISTS block_log_inserts;"

As with the tmpfs workaround, this is not a proper fix. It disables local persistent logging rather than fixing the underlying issue: the SQLite logging sink appears to ignore RUST_LOG=warn and continues persisting TRACE/DEBUG entries during normal streaming.

View original on GitHub ↗

18 Comments

christopherball · 3 months ago

I haven't dug into this issue with the same depth as you have, but I started noticing HUGE swings in my disk use over the past month (like 60GB less by end of a single day just doing light-weight work with codex). I eventually realized the "Code Helper" under "disk" within the Activity Monitor on MacOS was showing insane amounts of data being written to disk. This was happening even when I wasn't actively using VSCode (just remained open from past completed Codex work). In my particular case, even after closing VSCode and restarting, the disk space wasn't reclaimed.

It took some extra time to realize this bug was perfectly paired with the default setting of Time Machine holding onto local hourly snapshots on disk so I went ahead and did a backup, manually deleted all local snapshots, and set Time Machine to manual so as to avoid compounding this particular Codex bug.

Given its current state, I am literally going to stop using Codex for now, as it aggressively writes to disk all the time and there's nothing I can do about it. Apologies if what I've described isn't an exact match, but given the overlap of the extension writing to disk at aggressive rates, it felt related.

clemisch · 2 months ago

I also see this behavior. Do you still have this problem? Could you fix it by symlinking to a tmpfs? Just linking logs_2.sqlite to tmpfs didn't solve the apparent disk writes for me (unless Linux reports writes to tmpfs equally??)

I searched for similar issues and there are mutliple, but no reaction at all from the devs yet. These issues are a month old already 😬

piotrkacala · 2 months ago
Could you fix it by symlinking to a tmpfs?

It's a workaround but yes, it works. Just make sure that your /tmp is using RAM. Some distros have it set to HDD/SDD.

clemisch · 2 months ago

Thanks. I ended up just symlinking the whole ~/.codex folder (and copying it back before shutdown, hopefully...). /tmp is indeed a tmpfs for me.

niklassheth · 2 months ago

I also found this while investigating Codex's CPU usage, writing these trace level logs burns a lot of CPU. A way to reduce the logging level should be added to speed up Codex on older devices.

synap5e · 1 month ago
sqlite3 ~/.codex/logs_2.sqlite "CREATE TRIGGER IF NOT EXISTS block_log_inserts BEFORE INSERT ON logs BEGIN SELECT RAISE(IGNORE); END;"
niklassheth · 1 month ago
`` sqlite3 ~/.codex/logs_2.sqlite "CREATE TRIGGER IF NOT EXISTS block_log_inserts BEFORE INSERT ON logs BEGIN SELECT RAISE(IGNORE); END;" ``

I tested this and it helps a lot, but there's some more CPU savings from disabling the logging fully.

Each Codex instance was prompted to edit a json file.

Mode | Log rows | Log DB size | time CPU | perf user task-clock | SQLite worker
-- | -- | -- | -- | -- | --
Full inserts | 1888 | ~3.8 MB | 3.64s | 2.394s | 0.657s
Insert ignored | 0 | ~40 KB | 2.42s | 1.576s | 0.061s
Fully disabled | 0 | ~40 KB | 2.01s | 1.061s | 0 samples

clemisch · 1 month ago
but there's some more CPU savings from disabling the logging fully.

how did you do that?

niklassheth · 1 month ago

I patched the codex source to remove the log db's initialization.

<details>
<summary>Patch: disable log DB initialization</summary>

diff --git a/codex-rs/app-server/src/lib.rs b/codex-rs/app-server/src/lib.rs
--- a/codex-rs/app-server/src/lib.rs
+++ b/codex-rs/app-server/src/lib.rs
@@ -600,7 +600,7 @@ pub async fn run_main_with_transport_options(
 
     let feedback_layer = feedback.logger_layer();
     let feedback_metadata_layer = feedback.metadata_layer();
-    let log_db = state_db.clone().map(log_db::start);
+    let log_db = None;
     let log_db_layer = log_db
         .clone()
         .map(|layer| layer.with_filter(Targets::new().with_default(Level::TRACE)));
diff --git a/codex-rs/tui/src/lib.rs b/codex-rs/tui/src/lib.rs
--- a/codex-rs/tui/src/lib.rs
+++ b/codex-rs/tui/src/lib.rs
@@ -1047,7 +1047,7 @@ pub async fn run_main(
 
     let otel_tracing_layer = otel.as_ref().and_then(|o| o.tracing_layer());
 
-    let log_db = state_db.clone().map(log_db::start);
+    let log_db = None;
     let log_db_layer = log_db
         .clone()
         .map(|layer| layer.with_filter(Targets::new().with_default(Level::TRACE)));

</details>

emaadmanzoor · 1 month ago

Useful workarounds here when using Codex on a Raspberry Pi with SD card storage, thanks!

insilications · 1 month ago

This kind of program behavior is disrespectful in general, especially toward users with SSDs.

Gerry9000 · 1 month ago

There's already a shortage of SSDs, plz fix.

adamgwinn · 29 days ago

I can reproduce this on Windows Desktop.

Environment:

  • Codex Desktop: OpenAI.Codex 26.616.6631.0
  • CLI: codex-cli 0.128.0
  • OS: Microsoft Windows NT 10.0.26200.0
  • User env: RUST_LOG=warn, LOG_LEVEL=warn
  • config.toml: [analytics] enabled = false
  • Log DB had been freshly rotated before the second measurement.

Before workaround:

  • Previous logs_2.sqlite had grown to about 2.0 GB, with about 1.47 GB of freelist bloat.
  • After rotation/restart, a fresh DB still logged heavily while active.
  • One 60-second sample had 1,734 rows:
  • 950 TRACE log
  • 210 INFO codex_otel.log_only
  • 207 INFO codex_otel.trace_safe
  • 202 TRACE codex_api::endpoint::responses_websocket
  • 65 TRACE codex_api::sse::responses
  • smaller TRACE/DEBUG targets as well
  • No active git.exe storm was present during this sample.

Workaround tested:

CREATE TRIGGER codex_drop_noisy_logs
BEFORE INSERT ON logs
WHEN NEW.level IN ('TRACE', 'DEBUG')
  OR NEW.target IN ('codex_otel.log_only', 'codex_otel.trace_safe')
BEGIN
  SELECT RAISE(IGNORE);
END;

Result:

  • After waiting more than 60 seconds, those noisy targets stopped being persisted as logs rows.
  • A later 60-second sample contained only low-volume preserved INFO/WARN rows, roughly 30-40 rows/min in an active session.
  • This is only a local DB workaround. Codex still appears to emit/attempt these events; the real fix probably needs to apply the effective log-level filter before the SQLite sink, or expose a supported setting to disable/rate-limit the local SQLite log sink.
Necmttn · 29 days ago

The SQLite sink needs to respect an effective persisted-log filter, not only the process RUST_LOG. I would log one startup row showing requested filter, effective DB filter, targets enabled, and whether transport payload logging is on. Streaming diagnostics should be durable as counters and error events, while raw TRACE frames stay off unless explicitly enabled with a byte cap.

---

_Generated with ax._

zhuhaow · 28 days ago

I made a small unofficial tool for running community-found Codex workarounds:

https://codexfixes.com

Run it with:

npx codex-fixes@latest

So we can stop checking issue threads every day for months and just patch the annoying stuff when the community finds a workaround.

Master0fFate · 22 days ago

Reproduced on Windows Codex Desktop on 2026-06-29 00:40:10 CEDT +0200.

RUST_LOG=warn was set.
~/.codex/logs_2.sqlite was 689,213,440 bytes.
logs_2.sqlite-wal was 5,500,232 bytes.
logs table had ~207k rows, mostly TRACE.
After blocking inserts + VACUUM + WAL truncate, DB dropped to 40,960 bytes and WAL to 0.

yu20120707 · 5 days ago

Windows Desktop confirmation, same failure mode on Windows.

  • Codex Desktop: OpenAI.Codex_26.707.9564.0_x64
  • RUST_LOG=warn was configured in both .codex/.env and the user environment.
  • Before workaround: 15-second samples showed id_delta=229 with count_delta=0, and another sample grew the WAL by 26,199,080 bytes (~25.0 MiB).
  • TRACE sources included rmcp::service, codex_api::sse::responses, and hyper_util.

I then applied the community block_log_inserts BEFORE INSERT trigger. With Codex still running, a fresh 15-second read-only sample showed id_delta=0, count_delta=0, and wal_delta_bytes=0.

This is an effective local stopgap but disables persistent local diagnostics. The upstream fix should still make the SQLite sink honor RUST_LOG or avoid persisting high-volume SSE/WebSocket TRACE events by default.

Bob-Student · 2 days ago

macOS confirmation with local quantitative evidence.

Environment:

  • macOS on Apple Silicon
  • Codex CLI reported codex-cli 0.145.0-alpha.18
  • Desktop/app-server uses ~/.codex/logs_2.sqlite

Historical snapshot before the insert-blocking workaround took effect:

  • logs_2.sqlite: 223,064,064 bytes (~212.7 MiB)
  • WAL: 4,148,872 bytes
  • Retained rows: 81,041
  • Total estimated_bytes: 147,795,856
  • TRACE: 38,733 rows / 100,151,645 estimated bytes
  • INFO: 32,598 rows / 42,269,296 estimated bytes
  • DEBUG: 8,488 rows / 4,580,166 estimated bytes
  • Current sqlite_sequence for logs: 11,634,061

The large sequence value relative to retained rows is consistent with substantial insert-and-prune churn. The database file remaining around 213 MiB therefore does not reflect cumulative physical writes.

The community block_log_inserts trigger is currently installed. During active Desktop streaming, read-only samples showed:

  • no change in row count, MAX(id), or sqlite_sequence
  • no sustained WAL growth
  • the log table is now empty
  • 99.86% of the 54,459 database pages are on the freelist

This supports the trigger being an effective stopgap on macOS as well, but it disables persistent diagnostics and is not an acceptable long-term product fix.

Please prioritize an upstream fix that:

  1. does not persist TRACE/DEBUG SSE, WebSocket, and networking events by default;
  2. makes the SQLite sink honor the effective log filter;
  3. exposes a supported setting to disable or cap persistent diagnostic logging;
  4. bounds retention and WAL/checkpoint write amplification.

All measurements above were collected read-only; no VACUUM, checkpoint, deletion, or database rebuild was performed during verification.