Skip to content

[Bug]: Backend child OOM-crashes while writing a 307 MB usage-scan cache (usage-scan-cache-v11.json) #34018

Closed

Description

@6vers

Summary

The desktop backend child process aborts with JavaScript heap out of memory after about 4 hours of uptime. The crash happens while the backend writes the usage-scan cache. That cache has grown to 307 MB, and it looks like the backend serializes it into one string and then UTF-8 encodes it on the main heap.

Environment

  • T3 Code (Nightly) 0.1.9-nightly.20261110.4465, Electron, ARM64
  • macOS 27.2 (26B123), Mac17,6
  • Backend child uptime at crash: 14,291,290 ms (~4 h)

What happened

The desktop-backend-child log (~/.t3/userdata/logs/server-child.log) shows:

[...] Mark-Compact (reduce) 1452.3 (1717.8) -> 1452.1 (1618.6) MB, ... last resort; GC in old space requested
[...] Mark-Compact (reduce) 1452.1 (1618.6) -> 1448.4 (1588.3) MB, ... last resort; GC in old space requested

FATAL ERROR: CALL_AND_RETRY_LAST Allocation failed - JavaScript heap out of memory
...
pid=<backend> killed by SIGABRT

The desktop shell restarted the backend. The window survived.

Top of the native stack (macOS crash report, main thread):

node::OOMErrorHandler(char const*, v8::OOMDetails const&)
...
v8::String::Utf8Length(v8::Isolate*) const
node::encoding_binding::BindingData::EncodeUtf8String(...)
<JS frames>
...
node::fs::FSReqCallback::Resolve(v8::Local<v8::Value>)
node::fs::AfterStringPath(uv_fs_s*)
uv__work_done

So a JS continuation that runs after an fs call returns a path (mkdtemp, realpath or mkdir with recursive) UTF-8 encodes a very large string, and that allocation does not fit.

Evidence that it is the usage-scan cache write

State of ~/.t3/userdata/ right after the crash:

usage-scan-cache-v11.json          307M   (mtime = crash minute)
usage-scan-cache-v11.json.g4Znez/  empty dir, created at the crash minute
usage-scan-cache-v5.json           221M   (stale, from 20 Oct)
usage-scan-cache.json              147M   (stale, from 11 Oct)

The empty usage-scan-cache-v11.json.XXXXXX folder looks like the mkdtemp step of an atomic write (temp folder, write, rename). The process died between mkdtemp and the write. That matches the AfterStringPath, then EncodeUtf8String stack. A 307 MB JSON string plus a buffer of the same size, on a heap that already holds about 1.45 GB, exceeds the heap limit.

I did not capture a JS stack, so the exact function is inferred from the temp-folder name and the timing.

Expected

The cache write should not be able to crash the backend, whatever the cache size.

Suggested fixes

  1. Stream the cache to disk (for example NDJSON, or a stream with incremental JSON serialization) instead of JSON.stringify of the whole object followed by writeFile. Or move the cache into the existing SQLite store.
  2. Bound the cache: evict or compact old entries so it does not grow without limit (it went from 147 MB to 307 MB over 3 cache versions in about 1 month).
  3. Delete superseded cache versions (usage-scan-cache.json, -v5.json) on migration. They waste 368 MB here.
  4. Clean up the orphaned *.json.XXXXXX temp folder on startup.
  5. Add a regression test that writes a cache larger than the heap headroom (or that asserts the writer never builds the full payload as one string).

Activity

  1. added a commit that references this issue on Nov 10, 2026
    00231af
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions