Current behavior
In cypress run on any Chromium-family browser (we used Chrome 114 and Electron 138), with testIsolation: false and specs whose tests each cy.visit() a large, DOM-heavy SPA, the renderer process reliably dies mid-spec with:
We detected that the Chrome Renderer process just crashed.
CDP reports { status: 'crashed', errorCode: 133 } (= 128 + SIGTRAP). One crash per substantive spec: Cypress replaces the renderer, skips the remaining tests (Passing: N, Failing: 1, Skipped: rest), the next spec starts fresh and repeats the same arc. Across our suite (~950 tests), essentially every substantial spec crashed on every run. Small specs never crash.
What the crash actually is (from minidumps): we captured Crashpad minidumps of 21 consecutive crashes (BREAKPAD_DUMP_LOCATION). All 21 are SIGTRAP raised by Chromium's ImmediateCrash() on PartitionAlloc's OOM path, and all 21 carry the crash key page-allocator-mapped-size = 43,487,285,248 (byte-identical) i.e. PartitionAlloc's compiled-in virtual address pools (~40.5 GiB) are fully exhausted and the next PA allocation aborts. It is address space, not memory: RSS at death ranged 2.4–14 GB with 2.6–17 GB system memory free. Renderer VMA counts corroborate: crashing specs peaked at 43k–107k mappings . This path is silent in release builds: stderr, --enable-logging --v=1, and dmesg all show nothing; only Crashpad catches it.
Growth pattern: the Chrome process group stays flat (~1.1–1.3 GB) for most of a spec, then explodes in the final 10–30 seconds (measured 1.28 GB → 3.93 GB → 7.98 GB across consecutive 10 s profiler ticks) while the renderer main thread is wedged loading the SPA. With DEBUG enabled, the consistent precursor is the cri-client hang-detection line:
command Runtime.evaluate to target <id> has not resolved after 10000ms (_crashed: false)
Other things we tried
These aren't related to this issue, but worth trying if someone reading this has a related issue.
experimentalMemoryManagement on/off - Slightly reduced crashes but no real difference.
numTestsKeptInMemory: 0, - Had this before, does not free up.
screenshotOnRunFailure: false
--js-flags=--max-old-space-size=2048/4096 (it is not the V8 heap, V8 OOM prints Last few GCs to stderr; ours was captured and empty)
- --disable-gpu` on/off,
- --headless=old
(renderer still dies; Chromium just emits noTarget.targetCrashed`, so the run hangs instead)
ulimit -v unlimited
Desired behavior
Server→browser CDP socket messages should not retain anything in the renderer. Any of these fixes the leak:
- Add
returnByValue: true to the Runtime.evaluate call in CDPSocket.emit (packages/socket/lib/node/cdp-socket.ts), the result is only debug-logged, so serializing the Promise as {} is harmless and no RemoteObject is minted. (One-line fix.)
- Or make the evaluated expression complete with a primitive (e.g. append
; undefined, or have the installed send return undefined).
- Or pass an
objectGroup and periodically Runtime.releaseObjectGroup.
For more information, refer to the "Other" section
Test code to reproduce
The leak is per-message and browser-lifetime-per-context, so it needs volume to become visible, but the mechanism can be observed immediately:
cypress run (Chrome or Electron), testIsolation: false, one spec with many tests, each test doing cy.visit() of a DOM-heavy page followed by a normal amount of commands (each command's log events are socket messages; each proxied request is more).
- Observe renderer address space grow monotonically per test:
wc -l /proc/<renderer pid>/maps and/or the page-allocator-mapped-size crash key on death. On 13.17.0 the same spec stays flat.
- Direct observation of the mechanism without a big app: run any spec, take a heap snapshot of the renderer (or evaluate with the same CDP session) and observe one pinned Promise per socket message accumulating in the inspector's remote-object map; or simply log
Runtime.evaluate results server-side, every one is { result: { type: 'object', subtype: 'promise', objectId: ... } } and no Runtime.releaseObject/releaseObjectGroup is ever issued.
Our production-scale reproduction is a private CI suite; crash cadence there was ~1 renderer crash per spec, 100% reproducible across four 15.x versions, and 0/946 on 13.17.0.
Cypress Version
15.18.0 (also reproduced on 15.3.0, 15.16.0, 15.17.0; last known good: 13.17.0)
Debug Logs
With `DEBUG=cypress:server:browsers:cri-client` the crash is always preceded by the hang-detection line for the stalled transport evaluate:
cypress:server:browsers:cri-client command Runtime.evaluate to target 3AE4D0E37C7A331B3768C4A0E4FD132F has not resolved after 10000ms (_crashed: false)
repeated for 10–40+ s while renderer RSS balloons (profiler: 1.28 GB → 3.93 GB → 7.98 GB across consecutive 10 s ticks), then `Target.targetCrashed` / `{ status: 'crashed', errorCode: 133 }`.
`DEBUG=cypress-verbose:server:socket:cdp-socket` shows the per-message `Runtime.evaluate` sends ("sending message to browser") whose results are the pinned Promises.
Crashpad minidumps (21) with the byte-identical `page-allocator-mapped-size = 43487285248`
Other
Between 13.x and 14/15, send in packages/socket/lib/browser/cdp-browser.ts (13.x: packages/socket/lib/cdp-browser.ts) was changed from a synchronous arrow function to an async one.
For Chromium-family browsers, all server→browser socket.io traffic is emulated over CDP (CDPSocketServer / CDPSocket in packages/socket/lib/node/cdp-socket.ts): each message is delivered as a fire-and-forget Runtime.evaluate whose expression embeds the JSON-encoded payload:
// cdp-socket.ts (emit)
const expression = `
if (window['cypressSocket-${this._namespace}'] && window['cypressSocket-${this._namespace}'].send) {
window['cypressSocket-${this._namespace}'].send('${JSON.stringify(encoded)...}')
}
`
this._cdpClient?.send('Runtime.evaluate', { expression, contextId: this._executionContextId })
Note: no returnByValue, no awaitPromise, no objectGroup, and the result is only ever debug-logged. Runtime.releaseObjectGroup is never called.
The completion value of that evaluated program is the return value of send(...):
- Cypress 13.17.0;
cdp-browser.ts: const send = (payload) => { ... } (sync block, returns undefined). Completion value is a primitive → CDP returns it by value → no renderer-side handle. Zero crashes.
- Cypress 14/15 (through 15.18.0);
const send = async (payload) => { ... }. Completion value is a Promise. Per DevTools protocol semantics, Runtime.evaluate without returnByValue wraps an object result in a RemoteObject and the renderer's V8 inspector holds a strong reference to it in the default object group until the execution context is destroyed It should be noted that this is intended CDP behavior, lifetime management is the caller's job, and real DevTools clients pass an objectGroup and release it.
With testIsolation: false, the top-level execution context lives for the entire spec, so every single server→browser socket message, every log:added/log:changed relayed to the in-page reporter, every proxy request event, every automation response, plus the per-message ack echo CDPBrowserSocket.send generates, which permanently pins one Promise plus its resolution graph (the parsed payload, the decoded socket.io args, closures) in the renderer. On a command- and request-heavy suite this is thousands of pinned object graphs per spec. The address space they and their allocation churn consume ratchets up until PartitionAlloc's fixed pool cap, and the abort lands wherever the next allocation burst happens. For our specs, that's always the next cy.visit().
We know this is the case because we patched that one line browser side in our CI environment. We patched the served runner/extension bundles so send keeps its async body but the assigned function returns undefined. Without that patch, we hit ~86 crashes on a full run. With the one line change, we hit zero.
If anyone needs that code until this is patched, it's
# applied to packages/app/dist/assets/index-*.js and packages/extension/app-dist/*/background.js
# inside the Cypress binary cache (the server-side @packages/socket module ships only inside the
# v8 snapshot on Linux, so we patched the browser side, which is served from disk)
# minified bundle form: `X.send||(X.send=Y)` where Y is the async send fn
perl -0pi -e 's/\.send\|\|\((\w+)\.send=(\w+)\)/.send||($1.send=(console.error("CDP-SEND PATCH ACTIVE"),function(p){void $2(p)}))/g' "$f"
# unminified source form (cdp-browser.js style): `cypressSocket.send = send;`
perl -0pi -e 's/cypressSocket\.send = send;/cypressSocket.send = (console.error("CDP-SEND PATCH ACTIVE"),function(p){void send(p)});/g' "$f"
This function replaces the code
if (!cypressSocket.send) {
cypressSocket.send = send;
}
with
if (!cypressSocket.send) {
cypressSocket.send = (console.error("CDP-SEND PATCH ACTIVE"),function(p){void send(p)});
}
So that it discards the promise it gets from the async function. The two lines are for the minified and unminified versions. This should change anything visually unless you have DEBUG set to verbose.
A couple last notes
Current behavior
In
cypress runon any Chromium-family browser (we used Chrome 114 and Electron 138), withtestIsolation: falseand specs whose tests eachcy.visit()a large, DOM-heavy SPA, the renderer process reliably dies mid-spec with:CDP reports
{ status: 'crashed', errorCode: 133 }(= 128 + SIGTRAP). One crash per substantive spec: Cypress replaces the renderer, skips the remaining tests (Passing: N, Failing: 1, Skipped: rest), the next spec starts fresh and repeats the same arc. Across our suite (~950 tests), essentially every substantial spec crashed on every run. Small specs never crash.What the crash actually is (from minidumps): we captured Crashpad minidumps of 21 consecutive crashes (
BREAKPAD_DUMP_LOCATION). All 21 are SIGTRAP raised by Chromium'sImmediateCrash()on PartitionAlloc's OOM path, and all 21 carry the crash keypage-allocator-mapped-size = 43,487,285,248(byte-identical) i.e. PartitionAlloc's compiled-in virtual address pools (~40.5 GiB) are fully exhausted and the next PA allocation aborts. It is address space, not memory: RSS at death ranged 2.4–14 GB with 2.6–17 GB system memory free. Renderer VMA counts corroborate: crashing specs peaked at 43k–107k mappings . This path is silent in release builds: stderr,--enable-logging --v=1, and dmesg all show nothing; only Crashpad catches it.Growth pattern: the Chrome process group stays flat (~1.1–1.3 GB) for most of a spec, then explodes in the final 10–30 seconds (measured 1.28 GB → 3.93 GB → 7.98 GB across consecutive 10 s profiler ticks) while the renderer main thread is wedged loading the SPA. With
DEBUGenabled, the consistent precursor is the cri-client hang-detection line:Other things we tried
These aren't related to this issue, but worth trying if someone reading this has a related issue.
experimentalMemoryManagement on/off- Slightly reduced crashes but no real difference.numTestsKeptInMemory: 0, - Had this before, does not free up.screenshotOnRunFailure: false--js-flags=--max-old-space-size=2048/4096(it is not the V8 heap, V8 OOM printsLast few GCsto stderr; ours was captured and empty)(renderer still dies; Chromium just emits noTarget.targetCrashed`, so the run hangs instead)ulimit -vunlimitedDesired behavior
Server→browser CDP socket messages should not retain anything in the renderer. Any of these fixes the leak:
returnByValue: trueto theRuntime.evaluatecall inCDPSocket.emit(packages/socket/lib/node/cdp-socket.ts), the result is only debug-logged, so serializing the Promise as{}is harmless and noRemoteObjectis minted. (One-line fix.); undefined, or have the installedsendreturnundefined).objectGroupand periodicallyRuntime.releaseObjectGroup.For more information, refer to the "Other" section
Test code to reproduce
The leak is per-message and browser-lifetime-per-context, so it needs volume to become visible, but the mechanism can be observed immediately:
cypress run(Chrome or Electron),testIsolation: false, one spec with many tests, each test doingcy.visit()of a DOM-heavy page followed by a normal amount of commands (each command's log events are socket messages; each proxied request is more).wc -l /proc/<renderer pid>/mapsand/or thepage-allocator-mapped-sizecrash key on death. On 13.17.0 the same spec stays flat.Runtime.evaluateresults server-side, every one is{ result: { type: 'object', subtype: 'promise', objectId: ... } }and noRuntime.releaseObject/releaseObjectGroupis ever issued.Our production-scale reproduction is a private CI suite; crash cadence there was ~1 renderer crash per spec, 100% reproducible across four 15.x versions, and 0/946 on 13.17.0.
Cypress Version
15.18.0 (also reproduced on 15.3.0, 15.16.0, 15.17.0; last known good: 13.17.0)
Debug Logs
Other
Between 13.x and 14/15,
sendinpackages/socket/lib/browser/cdp-browser.ts(13.x:packages/socket/lib/cdp-browser.ts) was changed from a synchronous arrow function to anasyncone.For Chromium-family browsers, all server→browser socket.io traffic is emulated over CDP (
CDPSocketServer/CDPSocketinpackages/socket/lib/node/cdp-socket.ts): each message is delivered as a fire-and-forgetRuntime.evaluatewhose expression embeds the JSON-encoded payload:Note: no
returnByValue, noawaitPromise, noobjectGroup, and the result is only ever debug-logged.Runtime.releaseObjectGroupis never called.The completion value of that evaluated program is the return value of
send(...):cdp-browser.ts:const send = (payload) => { ... }(sync block, returnsundefined). Completion value is a primitive → CDP returns it by value → no renderer-side handle. Zero crashes.const send = async (payload) => { ... }. Completion value is a Promise. Per DevTools protocol semantics,Runtime.evaluatewithoutreturnByValuewraps an object result in aRemoteObjectand the renderer's V8 inspector holds a strong reference to it in the default object group until the execution context is destroyed It should be noted that this is intended CDP behavior, lifetime management is the caller's job, and real DevTools clients pass anobjectGroupand release it.With
testIsolation: false, the top-level execution context lives for the entire spec, so every single server→browser socket message, everylog:added/log:changedrelayed to the in-page reporter, every proxy request event, every automation response, plus the per-message ack echoCDPBrowserSocket.sendgenerates, which permanently pins one Promise plus its resolution graph (the parsed payload, the decoded socket.io args, closures) in the renderer. On a command- and request-heavy suite this is thousands of pinned object graphs per spec. The address space they and their allocation churn consume ratchets up until PartitionAlloc's fixed pool cap, and the abort lands wherever the next allocation burst happens. For our specs, that's always the nextcy.visit().We know this is the case because we patched that one line browser side in our CI environment. We patched the served runner/extension bundles so
sendkeeps its async body but the assigned function returnsundefined. Without that patch, we hit ~86 crashes on a full run. With the one line change, we hit zero.If anyone needs that code until this is patched, it's
This function replaces the code
with
So that it discards the promise it gets from the async function. The two lines are for the minified and unminified versions. This should change anything visually unless you have DEBUG set to verbose.
A couple last notes
Runtime.evaluateresults withoutreturnByValueare intentionally retained until released or context destruction. The regression is that the 14.x async refactor ofcdp-browser.tschanged the transport's evaluate completion value fromundefinedto a Promise without anyone managing the resulting handle lifetime.testIsolation: false+ long specs + Chromium show unexplained renderer growth/crashes thatnumTestsKeptInMemory/experimentalMemoryManagementdon't help. OOM issues 100% of the time on spec files with many tests - Cypress 15.4.0+ #33296 looks to me to be similar, and possibly Chrome/Edge renderer crash ("Aw Snap") from ResizeObserver loop triggered by Cypress environment — app works fine outside Cypress #34218consoleProps) hit the ~40.5 GiB PartitionAlloc pool cap sooner; quiet specs may only ever see it as slow growth.