[BUG] Design window renderer grows unbounded during generation and is killed at 2-4GB (macOS, Apple Silicon)

Status Open
Maintainer reply None cached
Activity 12 comments · opened Sep 11, 2026

Preflight Checklist

  • [x] I have searched existing issues and this hasn't been reported yet
  • [x] This is a single bug report (please file separate reports for different bugs)
  • [x] I am using the latest version of Claude Code

What's Wrong?

What's Wrong?

The Design window goes blank mid-generation. The renderer process for
that window grows to several GB over roughly a minute, then is killed by
the OS. The window is left blank with no content and no way to reload it
(Cmd+R only affects the chat window). On reopening Design, the session
shows "We got interrupted — the work paused before finishing" with
Resume/Dismiss.

This is NOT the known GPU-compositing or post-update-cache blank screen.
gpu-info.json shows the GPU healthy and active (ANGLE Metal, Apple M4,
active: true, init 143ms). No GPU process crash is logged.

Evidence

Four occurrences in ~20 minutes, from [process-memory] samples in
main.log. Each shows a renderer climbing then disappearing, with system
free memory jumping at the moment it dies:

  • renderer 18247: 258MB (10:25) -> 2241MB (10:26) -> 2081MB (10:27) ->

gone (10:28), sys_free 1.8GB -> 6.6GB

  • renderer 22414: 254MB (10:34) -> 2329MB (10:35) -> gone (10:38),

sys_free -> 7.0GB

  • renderer 22767: 4219MB (10:39) -> gone (10:40)
  • renderer 23052: 644MB (10:43) -> 4037MB (10:44) -> gone (10:45),

tree_rss 6548MB -> 1885MB, sys_free -> 7.4GB

Two peaked at ~4.0GB (4037MB, 4219MB), suggesting a hard per-renderer
ceiling. The two that died near 2.2GB appear to be system memory
pressure instead — sys_free_raw sits at 66-120MB for long stretches and
there is an explicit "trigger=pressure" sample at 10:12:40.

The 10:45 memory sample is missing entirely, i.e. the sampler itself
could not be scheduled.

Crash handling

The only trace is an empty Sentry capture at the moment of death:

[error] Sentry caught: {
eventId: '...',
type: 'Unknown',
value: 'No message',
stack: undefined
}

No message, no stack — consistent with the process being killed
externally rather than throwing. There is no "renderer gone" log line
and no error boundary or reload affordance in the UI.

Possible contributing factor

A live preview server (local dev server, port 3000) was running in the
same session. At 10:43:17: "[Preview] Server did not become ready within
timeout". Renderer 23052 first appears 5 seconds later at 644MB and
quadruples within the minute. Unclear whether the ballooning process is
the Design canvas or the live preview pane — worth checking both.

What Should Happen

  1. The renderer should not grow unbounded during generation.
  2. If it is killed, the failure should be caught and surfaced, not left

as a silent blank window.

  1. The Design window should have a reload path (Cmd+R, or a reload

button in the error state).

Steps to Reproduce

  1. On a 16GB Apple Silicon Mac, open Claude Desktop and use it for a

while so baseline memory reaches ~4-5GB across ~40 child processes.

  1. Open Design and ask for a substantial generation (in my case,

reading a markdown file and producing design output).

  1. Watch ~/Library/Logs/Claude/main.log [process-memory] samples.
  2. The Design renderer climbs past 2GB; the window blanks when it is

killed.

Environment

  • App version: [FILL IN — Claude menu > About Claude]
  • macOS: [FILL IN — Apple menu > About This Mac]
  • Hardware: MacBook Air, Apple M4, 16GB unified memory
  • GPU: ANGLE Metal Renderer, driver 26.3.1, active: true
  • Is this a regression? Unknown

Additional Information

Workarounds found so far: restart the app before Design sessions to
lower the memory floor, close the live preview pane, and keep each turn
small. None of these are fixes — the renderer still grows, it just has
more room before it hits the ceiling.

What Should Happen?

The design console shouldn't go blank.

Error Messages/Logs

Steps to Reproduce

it happens every time I use the design console.

Claude Model

None

Is this a regression?

Yes, this worked in a previous version

Last Working Version

_No response_

Claude Code Version

Claude 1.52386.0 (1003ca) 2026-09-10T05:55:17.000Z

Platform

Anthropic API

Operating System

macOS

Terminal/Shell

Terminal.app (macOS)

Additional Information

_No response_

View original on GitHub ↗

12 Comments

serajcloud · 1 month ago

Same bug here, also started today (11 Sep 2026), on an Intel Mac, and I can add a Chrome crash dump that pins down the cause of the 4 GB ceiling you saw.

Also submitted through in-app feedback, ID 24d6bbe8-448e-4649-a493-87b01873974a.

Environment

  • iMac 5K Late 2015 (iMac17,1), quad-core i7, 32 GB RAM, AMD Radeon R9 M395 2 GB
  • macOS 12.7.6 (21H1320)
  • Chrome 150.0.7871.125 (dump captured here, on claude.ai)
  • Claude desktop 1.46388.3, Electron 42.10.0 / Chromium 148 (same symptom, no dump recorded)
  • System memory was not under pressure: swap under 30 MB, 92% free. So on this machine it is not the OS killing the process.

Chrome Crashpad dump, 12:39 EDT

The renderer died from a V8 out-of-memory abort, not an external kill. The 4 GB cap is V8's pointer-compression cage, which is why both of your peaks landed at about 4.0 GB regardless of installed RAM.

ptype: renderer
loaded-origin-0: https://claude.ai
web-frame-count: 30
v8-oom-location: MarkCompactCollector: young object promotion failed
v8-oom-is-main-isolate: true
v8-oom-main-cage-size: 4096.00 MB
v8-oom-main-cage-free-size: 0 B
v8-oom-memory-allocator-capacity: 4192.00 MB
v8-oom-memory-allocator-size: 4089.76 MB
v8-oom-old-space-size: ~266 to 290 MB
v8-oom-new-space-capacity: 32.00 MB
v8-oom-lo-space-size: 24.37 MB
v8-oom-code-space-size: 12.33 MB
v8-oom-malloced-peak-memory: 126.91 MB

Last GC lines before the abort:

[88618:0x11c00564000]    81511 ms: Scavenge (during sweeping) 4014.6 (4052.6) -> 4013.8 (4059.9) MB, pooled: 0.0 MB, 9.26 / 0.00 ms (average mu = 0.794, current mu = 0.570) allocation failure;
[88618:0x11c00564000]    81520 ms: Scavenge (during sweeping) 4015.3 (4061.4) -> 4014.5 (4075.4) MB, pooled: 0.0 MB, 6.73 / 0.00 ms (average mu = 0.794, current mu = 0.570) allocation failure;

Two things worth noting for whoever picks this up

  1. The cage filled 81 seconds after the renderer started, while the ordinary heap spaces (old, new, code, large-object) totalled under 400 MB. Roughly 3.7 GB was consumed inside the cage by something other than regular JS objects, which points at retained backing stores (typed arrays, canvas or image buffers) rather than object growth.
  2. The renderer had 30 frames, which matches a canvas with many artboards each in its own frame, all sharing one isolate and one 4 GB budget. Smaller canvases last longer here, consistent with that.

No GPU faults or driver resets in the system log, so I agree this is not the GPU blank-screen case.

Happy to share the full minidump on request, or capture a heap snapshot from a session before it dies if that would help.

lrinderknecht · 1 month ago

Thank you — this is the measurement I couldn't take. Yes to both offers: the full minidump, and especially a heap snapshot from a session before it dies. The snapshot is the more valuable of the two now, for the reason your dump implies: it should name the retainer, which is the one thing neither of us can see from outside.

Your dump corrects my report, and I'd rather say so than quietly revise. I had proposed the per-turn component recompile as the mechanism — the design-system project recompiles its component bundle at the end of every turn, and mine covers 222 components, so it seemed to fit. Your spaces rule it out: old 266–290 MB, new 32 MB, code 12.33 MB, lo 24.37 MB — under 400 MB of ordinary heap against a 4089.76 MB allocator. A compile that size would appear in old and code space. It doesn't. So I now agree with your reading: ~3.7 GB inside the cage is retained backing stores, not object growth, across frames that each hold imagery alive.

That also retroactively explains something I had mis-read as luck. My project had accumulated 587 unreferenced pasted screenshots (~3 GB decoded). Deleting them bought exactly one working session before the crashes resumed. I wrote that up as "buying headroom" — but on your account it's mechanistically direct: image backing stores are the very thing filling the cage, so removing 3 GB of them briefly moved the project back under 4096 MB. If that's right, it's also a crude confirmation of your retained-buffers hypothesis from the opposite direction.

What I can add that you don't have: the onset is a 90-minute window, and it clears the client. Reconstructed from timestamped commits in the repo that consumes this project, all times MDT, 11 Sep 2026:

Time | Event
-- | --
07:20–11:14 | Project answering normally — five replies delivered and consumed
11:14 | Last healthy reply
11:50 | New request placed in the project, delivery verified
12:42 | Crashing on every attempt
13:55 | Desktop app auto-updates — does not fix it

Desktop installs on my machine were 4 Sep 21:28, 8 Sep 14:52, 10 Sep 18:11, 11 Sep 13:55. So the app was on an unchanged build across the onset — installed the previous evening, working normally through 11:14 the next morning — and the update that followed didn't help. It fails in Chrome too.

Which is why your report matters beyond corroboration: you're on an Intel iMac, macOS 12.7.6, Chrome 150, 32 GB. I'm on Apple silicon, macOS 26.5.1, 16 GB. We share no hardware, no OS, and no browser build, and we failed the same day. Between that and the timeline, the trigger is neither client-side nor local — something deployed around midday on 11 September. Project contents can't be it either: 222 components did not appear at 11:30 that morning. Corpus growth is gradual and this onset wasn't.

My read is that project scale is a predisposing condition, not a cause — it explains why some projects were first over the edge, not why that edge moved. Consistent with your note that smaller canvases last longer.

Two questions, if you have time:

Roughly how many artboards/preview cards does your canvas carry? Mine registers 81 preview cards plus 31 templates, and your web-frame-count: 30 is the first hard number anyone has put on the frame side of this.
Do you have a small design-system project you can test? If a small one survives and a large one dies on the same account and the same day, that separates "scales with content" from "broken for everyone" — which is the difference between a workaround existing and not.
I've filed through in-app feedback as well and written the whole thing up as a report with the timeline, the ruled-out list and your dump credited. Happy to share that with anyone from Anthropic who picks this up, and happy to run any instrumentation you'd find useful on the Apple-silicon side for contrast.

lrinderknecht · 1 month ago

Update — I ran the control myself, same account and same browser, while the large
project was still failing: a 39-file design system of mine completed three escalating
turns, drawing artboards and swapping image assets, without trouble. A small Project
also completed. So the design-system surface is fine; it's specific to the large one.
Which sharpens your "smaller canvases last longer" observation into something
reproducible without either of our data — a synthetic design system somewhere in the
100–900 file range should do it.

serajcloud · 1 month ago

Answers to both questions, plus two things from my side that move the timeline.

1. Canvas size. The Large Project that dies here lists 38 pages in the Pages switcher, all .dc.html design cards. Two are heavy: a dashboard card at about 1.2 MB and a register card at about 264 KB. The dump's web-frame-count of 30 is therefore fewer than the page count, so not every card was mounted as a frame at the moment of death.

2. Control test. Brand-new empty project, same account, same Chrome, fresh session, one write per turn, tab kept in view 90 seconds after each:

| Step | Write | Result |
|---|---|---|
| 1 | step1.md, one line, 22 bytes | survived |
| 2 | step2.md, plain prose, 2.3 KB | survived |
| 3 | step3.md, plain prose, 8.4 KB | survived |
| 4 | Test Card.dc.html, one heading and one paragraph | survived (about 0.5 KB, and the tool auto-rendered it in the preview, so one design frame was mounted) |

For scale: an 8 to 9 KB tool write is the payload size that reliably kills the large project. So on this account the same action that dies in the 38-page project completes in an empty one. That matches your 39-file result and separates "scales with content" from "broken for everyone".

One more data point on the load side. Opening the large project in a fresh Chrome tab and just viewing a file, no generation, peaks the renderer at about 417 MB RSS and holds there. The growth needs an active turn. Viewing is not enough.

3. Onset on my side is earlier than yours. The desktop app on this Mac logs a per-process memory sample every minute and a Sentry capture when a renderer dies. Counting captures per day across the log:

| Day | Sentry captures |
|---|---|
| 1 to 10 Sep | 0 |
| 11 Sep | 15 |

Every one of the 15 lands at the moment a renderer vanishes, with the same empty payload you saw (type Unknown, No message). The first is at 08:48 MDT on 11 Sep, followed by a manual app restart at 09:06. Then 10:09, 10:21, 10:26, 10:43 MDT and so on through the evening. My Chrome dump is at 10:39 MDT. So this machine was already dying two and a half hours before your last healthy reply at 11:14 MDT. If your window holds for your account, the rollout was staged or gradual rather than a single global flip.

4. Large renderers were fine the day before. On 10 Sep the same desktop app had renderers at 4039 MB and 4778 MB RSS that lived for hours (one for 20 hours, into the morning of the 11th) and were only retired by an app quit. RSS includes memory outside the V8 cage, so those figures do not mean the cage was full, but they do show that a big Design project sitting at multi-GB was routine before the 11th and fatal from the 11th. Whatever changed, it changed how the cage fills, not how big projects are.

5. Minidump. Redacted copy (local username and two extension IDs replaced with same-length placeholders, nothing else touched, offsets intact) is here, base64 in a gist because gists are text only: https://gist.github.com/serajcloud/627ef23c8ae3e793db685e4fb51e1df1

Heap snapshot: I will try to capture one mid-turn on the large project before it dies and post it if it is usable. At 4 GB it may not be.

lrinderknecht · 1 month ago

This is decisive on the onset, and it reverses a retraction I'd made — I'd argued the
onset could be explained by per-chat accumulation without a release. Your capture counts
kill that: accumulation was happening before the 11th too and can't produce 0 → 15. And
the 10 Sep renderers at 4039/4778 MB living for twenty hours is the clincher. Your
sentence is the right frame for the whole thing: it changed how the cage fills, not how
big projects are.

Three things from my side.

  1. A second dump of mine, an hour after the first, argues your point from another angle:

the renderer's own uptime in the GC log.

08:35 dump 62,717,135 ms ≈ 17.4 hours
09:34 dump 280,655 ms ≈ 4 min 41 s (Chrome, large design system)

Same v8-oom-location in both (young object promotion failed), same main cage at
4096.00 MB with "ran out of reservation", and the accounted spaces tiny in both:

old-space-size 307.89 MB / 232.82 MB
code-space-size 12.33 MB / 8.52 MB
lo-space-size 1.58 MB / 1.83 MB
malloced-peak-memory 112.20 MB / 112.20 MB
memory-allocator-size 4101.33 MB / 4087.67 MB (capacity 4192.00 MB)

So ~3.8 GB sits outside the accounted spaces, and a FRESH renderer got there in under
five minutes. Seventeen hours versus 281 seconds, same wall. Hard to square with
anything about project size; easy to square with your conclusion.

  1. My 08:35 dump carried a v8-oom-stack, which I don't think yours quoted:

updateProjectData in .../design/v2/assets/v1/desktop-auth-wait-*.js
<none> in =:8283:56331
forEach
yU in =:8275:9698
onEvent in =
onToolDelta in =

Bottom-up: onToolDelta → onEvent → forEach → updateProjectData. Caveat: only one of
my two dumps recorded a stack, so treat it as one observation rather than a pattern.
It does line up with your finding that viewing holds at ~417 MB and growth needs an
active turn, and with an 8–9 KB tool write being the reliable trigger.

  1. Correcting myself: I'd said the desktop app dies sooner than Chrome. Chrome then died

in 281 seconds, so I'm withdrawing that. What does hold is that the app writes NO
minidump — my Claude.app Crashpad directory has stayed empty through every app crash
across two days, matching your "same symptom, no dump recorded". So reproduce in
Chrome for the evidence, not because it's safer.

Also in your separates-scales-with-content direction: a regular Project that references
my 950-file design system renders that library's templates fine, one at a time. Same
content, different surface. And a 39-file design system of mine survived three escalating
turns with artboards and image swaps.

Not pulling your minidump — that should go to Anthropic from you directly rather than
through me.

serajcloud · 1 month ago

Timed reproduction, with a memory curve, from the same large project (Large Project, 38 design pages) in the Claude desktop app on macOS 12.7.6, Intel, 32 GB. Today, 12 Sep 2026, times EDT.

Method. One write per turn to the same 264 KB design card (a findings register), each write a single block of 0.9 to 1.3 KB, waiting for the renderer to settle before the next. The Design window's renderer process was sampled every 5 seconds with ps (resident size), and the desktop app's own log was watched for its Sentry capture line, which fires at the moment a renderer dies. System free memory was over 8 GB throughout, so nothing here is OS pressure.

Result: three writes survived, the fourth killed the renderer.

| Write | First peak | Dip between waves | Second wave | Settled at |
|---|---|---|---|---|
| Cover line edit, 70 bytes | 2,824 MB | single wave | | 925 MB |
| Finding 1, 1.3 KB | ~4,690 MB | 1,764 MB | 3,298 MB | 996 MB |
| Finding 2, 1.1 KB | 4,147 MB | 3,329 MB | 3,755 MB | 1,035 MB |
| Finding 3, 0.9 KB | 4,948 MB | 2,663 MB | 3,090 MB | 1,068 MB |
| Finding 4, ~1 KB | 5,596 MB | no release | | died |

Curve for the fatal write, resident size of the Design renderer:

11:53:04  1068 MB   idle before the write
11:53:10  1905 MB
11:53:15  2613 MB
11:53:20  3717 MB
11:53:25  5596 MB   peak
11:53:30  5268 MB   held, no release
11:53:35  4843 MB
11:53:40  4810 MB
11:53:42  Sentry caught { type: 'Unknown', value: 'No message', stack: undefined }
11:53:43  process gone; the window's second renderer followed 10 s later

What it shows.

  1. Every write re-renders the whole card, and the cost scales with the card, not the edit. A 70 byte edit to the 264 KB card cost 2.8 GB. The same 1 KB block costs more each time as the card grows.
  2. Each surviving write shows two waves with a release in between. On the fatal write the first wave went higher than any before it and the release never came. The process sat between 4.8 and 5.6 GB for 17 seconds and was aborted.
  3. The idle floor barely moved, 925 to 1,068 MB over four writes. This is not a slow leak across turns. It is a transient allocation, proportional to the card being re-rendered, that on the fatal write failed to be freed.
  4. Resident size above 4 GB is survivable on its own. Three writes peaked past 4.1 GB and came back. What kills the process is whatever portion of that transient lives inside the V8 cage, which is why the Chrome dump earlier in this thread shows the cage at 0 B free with ordinary heap spaces under 400 MB.

For the reporter's question about a threshold: on this project the edge sits at a single card of about 264 KB after a handful of writes. Cards a fraction of that size (a 2 KB markdown file in the same project) peak around 2 GB and are fine. Splitting the register into a small active card and an archive is the workaround I am using.

CleImb · 1 month ago

Same bug on Brave 1.94.121 (Chromium 152) / Linux, claude.ai/design in the browser — third environment, and a sampling heap profile that names the allocation site independently of the crash stack posted above.

Timeline (matches the Sept 11 onset)

journalctl --user keeps every renderer OOM (V8 javascript OOM (MarkCompactCollector: young object promotion failed)). Zero occurrences Sept 8–10. First one Sept 11, 09:23 UTC, then 09:58, 14:16, and a series from 22:24 UTC onward (one crash every 2–6 minutes whenever a generation was running). Nothing on this machine changed in between: same browser build (installed Sept 8), same extensions; a private window (extensions off), a reboot and a fresh conversation changed nothing.

Renderer lifetime at death, from the <--- Last few GCs ---> trace: 145 s and 347 s from load to 4 GB in two of the crashes. Heap at death: 3678 MB → 4013 MB, scavenges reclaiming ~nothing.

Method

Brave launched with --remote-debugging-port; a CDP script attached to the claude.ai/design/p/… page and its *.claudeusercontent.com preview iframe (a separate OOPIF target), polling Performance.getMetrics every 10 s and running HeapProfiler.startSampling (128 KiB interval) on each, stopped before the crash.

Findings

  1. Idle: nothing leaks. Host page flat at 67 MB for 2+ min with the full chat and project loaded; preview iframe 16 MB. (Minor: the host adds ~1.7 JS event listeners per second at idle without releasing them — 1506 → 1773 in 160 s — unrelated to the crash, but it is a timer in the host.)
  2. The instant a prompt is sent, the host page churns. 68 → 651 MB in 45 s, then a sawtooth between ~250 MB and 2.3 GB for the whole generation.
  3. Sampled live allocations: 192 MB of 195 MB in a single function — updateProjectData @ design/v2/assets/v1/desktop-auth-wait-CTmH6dV0.js:8283. Call paths, by self size:
  • onToolDelta → g → onEvent → pe → yU → forEach → (anon) → updateProjectData — 100 MB
  • onTokens → … → updateProjectData — 41 MB
  • onToolBlockStart → … → updateProjectData — 12 MB
  • onToolBlockComplete → … → updateProjectData — 12 MB
  • handlePaste → dispatchTransaction → onChange → updateProjectData (ProjectPage) — 14 MB, i.e. typing/pasting a long prompt in the composer takes the same path (the "crashes while composing" reports fit).

So it is not only tool deltas: every streamed event (text tokens too) rebuilds the project snapshot.

  1. The preview iframe is not involved. It lives in another process (16 MB idle, ≤ 112 MB while a file it renders is rewritten), so canvases/artboards/React-dev/Babel inside the design pages do not count toward the host's 4 GB cage. All six pages of the project opened standalone in headless Chrome for 45–70 s stay at 10–30 MB.
  2. Project size scales the cost but does not cause it. Project before: 702 files / 47.6 MB (mostly PNG screenshots). After deleting 130 screenshots (572 files / 12 MB), a comparable multi-minute analysis task peaked at 867 MB instead of 2347 MB, with 0 readings above 1 GB instead of 11. But a mockup task that rewrote a 91 KB .jsx still hit 3970 MB and killed the tab; in another run the heap reached 3411 MB while the sampling profiler showed only 30 MB live — the rest was garbage the GC had not yet reclaimed. Peaks coincide with large tool writes/reads (2.0 GB, 1.8 GB, then 4 GB → dead, ~10 s apart). This is an allocation-rate problem (snapshot rebuilt per event, cost ∝ project data incl. the file being streamed → superlinear on big writes), not retention.

What would fix it on the client

Coalescing updateProjectData per animation frame / per N events, or structural sharing of the snapshot so a token delta does not copy the whole project, should remove the burst. As a user-side mitigation, keeping the project small and files under ~20–30 KB lowers the peaks but does not prevent the crash.

Raw sampling profiles (.heapprofile JSON) and the CDP script available on request.

---
Measurements, profiling and write-up by Claude Code (Claude Opus 5).

serajcloud · 1 month ago

Two more data points from the Intel Mac, both about when, which may help whoever is matching this against deploy logs. Times below are EDT (UTC minus 4) unless marked.

1. The regression's build window, from the client cache. The desktop app's Chromium cache keeps every version of the host bundle it has loaded, and the cache file's write time gives the first time each version arrived on this machine. Filtering for desktop-auth-wait-*.js, the bundle both crash stacks and the heap profile point at:

| First seen (EDT) | Bundle |
|---|---|
| 10 Sep 14:12 | desktop-auth-wait-CgALakKs.js |
| 11 Sep 02:22 | desktop-auth-wait-DvEsHF4k.js |
| 11 Sep 16:01 | desktop-auth-wait-DgMgrYEp.js |
| 11 Sep 19:18 | desktop-auth-wait-B6UlsIRa.js |
| 11 Sep 19:57 | desktop-auth-wait-CTmH6dV0.js (still live as of 12 Sep 13:10) |

My Chrome tab that died on 11 Sep at 12:39 had loaded DvEsHF4k at 12:38. All renderer deaths on this machine are on DvEsHF4k or later; none on CgALakKs or earlier, across ten days of cached versions back to 31 Aug. So the regression entered between the 10 Sep 14:12 EDT build and the one live by 02:22 EDT on the 11th (06:22 UTC), consistent with @CleImb's first crash at 09:23 UTC. The daily builds before that, 31 Aug through 7 Sep, were all fine here.

2. Something changed server-side today at about 16:44 UTC, and it roughly halved the cost and removed its dependence on payload size. Same project, same 196-turn chat, same client bundles (of 456 Design chunks in the cache, none new after that time, and the live page still loads CTmH6dV0), same machine. Only the Design window was closed and reopened. Measured as resident size of the Design renderer, sampled every 5 s:

| Time (UTC) | Streamed write | Peak | Outcome |
|---|---|---|---|
| 15:53 | ~1 KB block into a 264 KB card | 5,596 MB | died |
| 16:15 to 16:18 | three ~1 KB slices into a small file | 4,791 / 4,240 / 5,508 MB | survived, released to ~850 MB |
| 16:36 to 16:41 | long verifier turns, small payloads | 4,945 / 5,484 / 5,094 MB | survived |
| 16:44 | window closed and reopened | | |
| 16:54 | ~1 KB slice, same kind as 16:15 | 2,582 MB | survived |
| 17:15 | one-word str_replace on a 1.2 MB card | 2,201 MB | survived |
| 17:22 | 4,470 chars in one write_file | 2,428 MB | survived, sawtooth 1.7 to 2.4 GB for 60 s |
| 17:26 | 22,473 bytes in one write_file | 2,381 MB | survived, sawtooth for 80 s |
| 17:29 to 17:33 | 90,191 bytes in one write_file | 1,504 MB | survived, held 1.3 to 1.5 GB for nearly 4 minutes while streaming |

Before 16:44, every streamed write spiked to 4 to 5.5 GB and either released or died, and cost tracked the size of what streamed. After 16:44, peaks sit at or under 2.4 GB, a 22 KB stream costs the same as a 4.5 KB one, and a 90 KB stream costs less than either while taking four times as long. That is the shape of the per-event snapshot rebuild being coalesced, or of the server delivering far fewer deltas per write, and it did not come from the client. If there was a server-side change around 16:44 UTC on 12 Sep it would be worth confirming here, since anyone still on the old behavior would then be on a stale session or a different rollout slice.

Nothing else in the thread's picture changes. The mechanism @lrinderknecht and @CleImb traced still stands; this only narrows when it started and shows a partial mitigation landing.

serajcloud · 1 month ago

Follow-up on one path that deserves naming on its own: the composer. @CleImb's profile already listed handlePaste → dispatchTransaction → onChange → updateProjectData as one of the callers that rebuild the whole project snapshot, i.e. editing the prompt box takes the same path as a streamed tool delta. On an Intel Mac I can now put numbers on how large that gets, because with a big project loaded it is the single most expensive thing in the window, ahead of the actual writes.

Measured as resident size of the Design renderer, sampled every 5 s, nothing sent, no generation running, just characters entered into the composer with the project's large card mounted:

A single short line, about 70 characters, typed over roughly a minute:

15:28:16  2165 MB
15:28:21  3478 MB
15:28:31  4217 MB
15:28:36  4426 MB
15:28:56  4833 MB
15:29:06  4232 MB
15:29:16  4325 MB
15:29:21  1494 MB   (line finished / cleared)

That is a sustained sawtooth between roughly 3.3 and 4.8 GB for the whole time the line was being typed, on a renderer that idles under 1 GB. Each keystroke burst rebuilds the snapshot and the collector races to free the previous copy; while typing continues it does not fully win. The peak (4,833 MB) is within ~150 MB of the 4 GB-cage ceiling that kills the tab, from ~70 characters of typing and no send.

Two practical points:

  1. This is very likely the mechanism behind the "crashes while composing" reports. Under the pre-mitigation behavior (before the ~16:44 UTC change I described above), the same typing would have hit the wall outright rather than sawtoothing under it.
  2. User-side mitigation that works: draft anything longer than a word or two outside the app and paste it. A paste rebuilds the snapshot once (one spike, ~2 to 3 GB here) instead of once per keystroke burst. Typing in place is the expensive case.

Renderer age compounds it: a freshly reopened window sawtooths lower for the same typing than one that has been open an hour, so a periodic window reopen also helps. None of this is a fix, only headroom. The client-side fix is the coalescing @CleImb described; a composer onChange that rebuilds the entire project snapshot per edit is the clearest single instance of it.

lrinderknecht · 1 month ago

Ran your composer test on Apple silicon (Chrome 152, ~950-file design system, nothing
sent, no generation). Design renderer RSS every 2s:

idle after load 892 - 901 MB swing 9 MB
TYPING 90s 1195 - 3414 MB swing 2219 MB
idle, composer clear 961 - 973 MB swing 13 MB
PASTE 62 chars 977 - 1092 MB swing 115 MB
PASTE 9,177 bytes 1008 - 1132 MB swing ~155 MB

Your sawtooth reproduces exactly — sustained 1696–3414 MB for as long as keystrokes
continue, back to baseline within 10s of stopping. A select-all-delete is one bump of
+370 MB, released at once. One edit event, one rebuild.

The new bit: paste cost barely scales with payload. 148x more text for ~40 MB more
memory. So the rule isn't "keep pastes small" — it's that the number of edit events is
what costs, and pasting a 9 KB document is cheaper than typing one sentence. Worth
saying because "8–9 KB reliably kills it" is true of tool writes and does NOT transfer
to composer pastes; I'd assumed it did and was wrong.

Qualifier on the mitigation: one of my crashes happened ON a paste. But that dump shows
the renderer 4m41s old with the GC already thrashing at 3.6–3.8 GB and failing to
recover, so the paste tipped a full cage rather than filling it. Pasting is cheap;
nothing is safe once the renderer is near the ceiling.

One correction on your post: the 4,833 MB peak is RSS, and RSS includes memory outside
the cage — as you noted yourself re: the 4039/4778 MB renderers. Since the cage caps at
4096 MB, that peak can't be cage occupancy, so I don't think "within ~150 MB of the
ceiling" follows. The sawtooth stands on its own; I'd just drop the proximity claim.