Skip to content

Memory leak in --browserUrl mode under sustained screencast + evaluate_script load: V8 heap grows to ~4GB limit, server aborts (SIGABRT) #2291

Description

@jackzhouusa

Summary

A long-lived chrome-devtools-mcp server (spawned by an AI coding-agent CLI via npm exec chrome-devtools-mcp@latest) attached to an existing Chrome with --browserUrl steadily grows its JS heap until it hits the default ~4GB V8 heap limit and aborts. The npm exec wrapper process dies alongside it (unhandled-rejection path → uv_kill), so each event leaves a pair of crashed node processes.

This looks distinct from #1192 / #1214: those were --autoConnect-mode collector leaks fixed in March/April — and indeed we saw ~3 months of clean operation after those fixes. The leak reported here reappeared with 1.2.0+ in --browserUrl mode.

Environment

  • chrome-devtools-mcp: crashes observed on 1.2.0 and 1.4.0 (resolved fresh via npm exec chrome-devtools-mcp@latest on each start)
  • Invocation: npm exec chrome-devtools-mcp@latest -- --browserUrl=http://127.0.0.1:<port> --experimentalScreencast
  • node v23.6.0 (Homebrew), macOS 15.7.2 (Apple Silicon)
  • Chrome: current stable, attached via CDP (--browserUrl), long-lived instance

Workload profile

The server runs for hours-to-days under an agent that repeatedly: takes snapshots, runs frequent evaluate_script polling, and records repeated screencast_start / screencast_stop cycles against a canvas/WebGL-heavy page.

Observed behavior

Seven crash events over 19 days (2026-06-13 → 2026-07-01), all with the identical signature. Time-to-crash shrinks as automation load increases — process lifetime went 2.7 days → 12.7 h → 3.9 h across the last three events, consistent with a load-proportional leak rather than a fixed-rate one.

Faulting thread (named chrome-devtools-mcp), from the macOS crash report:

libsystem_c.dylib     abort
node                  node::OOMErrorHandler(char const*, v8::OOMDetails const&)
node                  v8::internal::V8::FatalProcessOutOfMemory(...)
node                  v8::internal::Heap::FatalProcessOutOfMemory(char const*)
node                  v8::internal::Heap::CollectGarbage(AllocationSpace, GarbageCollectionReason, ...)
node                  v8::internal::IncrementalMarkingJob::Task::RunInternal()
node                  node::PerIsolatePlatformData::RunForegroundTask(...)
libuv                 uv__async_io → uv__io_poll → uv_run

(Other events show the same FatalProcessOutOfMemory via MinorGCJob / mark-compact — i.e., GC can no longer reclaim; the heap is genuinely retained, not fragmentation.)

Secondary: when the server aborts, the parent npm exec wrapper crashes too, inside PromiseRejectReactionJobnode::Kill/uv_kill — a crash-on-crash in the wrapper's rejection handling that might be worth a defensive fix on its own.

What I've ruled out

  • Not system memory pressure: no jetsam kill; the stack is V8's internal heap-limit abort.
  • Not the screencast temp-file path: frames are written to disk (fs.mkdtemp → mp4), so suspicion falls on CDP session/protocol-object retention under sustained evaluate_script + screencast churn.
  • 1.5.0 changelog contains no related fix, so filing this.

Happy to provide

Full (redacted) .ips crash reports for all seven events, Crashpad minidumps, or a run with --heapsnapshot-near-heap-limit instrumentation if that helps pinpoint the retained objects.

Metadata

Metadata

Assignees

No one assigned

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions