DeepSeek Harness web OOM in ~50 min: memory leak fixes
DSH plugin web shows two memory symptoms: opening one large old session freezes the entire service (even GET / times out), or about 50 minutes of use ends in FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory — the cause is cold-history full materialization stacked on four unbounded accumulators. Both happen on the single Node main thread shared with the web server, which is why the symptom is usually "the whole UI is unresponsive" rather than one broken session. Physically moving large session directories out of sessions/ and keeping a single instance running is the most effective mitigation today.
DSH plugin symptoms: frozen cold session and OOM after ~50 minutes
The same design gap fires on two time scales: one-shot materialization when a history is opened, and steady accumulation while the process runs. Two community reproductions:
- Cold history pins the main thread:
dsh webprintshttp://127.0.0.1:3080and the listener exists, but HTTP requests time out at the ten-second mark (HTTP_ERROR after 10185ms); during the stall the node working set was ~163 MB, CPU was near saturation, and multipleCLOSE_WAITconnections piled up on port 3080. An A/B test isolated it to history loading: after moving the two affected session trees out of the active root and restarting, the firstGET /returned in 243 ms, warm requests in ~17 ms, at 0.02 CPU seconds (#1550). - Heap hits 4 GB and crashes at ~50 minutes: GC logs show the heap climbing from 4.0 GB to 4067/4090 MB over about 3024 seconds, with a single Mark-Compact taking 3.1 seconds during which the service was unresponsive, and exit code 134. Both
0.1.0-rc.5and0.1.1-rc.2reproduce it (#3876). - Boot materializes too: with 15 valid sessions (1.8–2.7 MB compressed each, ~300k tokens) in
sessions/<cwd>/,dsh webclimbed to ~3 GB of heap within 60–90 seconds with zero client interaction. About 60% of CPU samples landed instructuredClone, and a heap snapshot showed 21.6 million live objects (Object526 MB, strings 329 MB, plusassistant/chunk,reasoning-delta, and similar event objects). - On Linux the growth shows up as RSS, not JS heap: on
0.1.2-rc.1, a heavy 20-minute window produced five crashes; a live-process capture showed the V8 heap flat (213→206 MB) while RSS grew from 430 MB to 1.16 GB, with ~737 MB in glibc[heap]and allocation stacks inZSTD_decompressStreamandJsonStringifier::Extend. After the load stopped, RSS fell back from 1190 MB to 390 MB on its own.
DeepSeek Harness's four unbounded accumulators plus cold-history materialization
The write side has four containers that only grow, and the read side decodes an entire log before paginating; both share one event loop. Point by point:
FrameQueue.bufferhas no cap (packages/host/apiproxy/src/api-proxy.ts):push()enqueues unconditionally with no backpressure or dropping, one queue per mux/host subscriber, and eachsession/eventframe is copied into every subscriber queue. When a browser tab is suspended or consumes slowly (TCP still open), frames accumulate without limit, each holding a full event reference.Session.logis append-only (packages/core/session/src/index.ts): the event array never trims, so a long session's events stay resident forever.- Projection cache rows are never deleted (
packages/session/session-projection-cache/src/index.ts): onsession/disposedit only callsmarkClean(session)anddirty.delete(session), with no delete on the underlying table;KvTableis a fully resident in-memory Map whose row count grows with the number of past sessions. SessionWriteBehind.pendingkeeps failed writes forever (packages/session/session-persistence/src/write-behind.ts):this.pending = batch.concat(this.pending)retains the whole failed batch for retry with no cap, and every event isstructuredCloned.- The read-side materialization chain (#1550):
historySourceFor()callssessionPersistence.inspect(sessionId)and gets the full event array beforepaginateruns; underneath,readPrefix→readZstdPrefixdecodes every frame andscanLogparses the entire buffer; thenprepareCore→adoptStoredEventsmigrates and deep-freezes every event; thenSession.create/fromRestore→adoptSessionEvent(structuredClone(event))clones each one. A single event is deep-cloned at least 2–3 times per prepare, on top of the full seed copyseed: loaded.events.map((event) => structuredClone(event))(dsh-session-persistencelib/index.js:1390and:1586, still present in 0.1.2-rc.1). - The per-read cost is measurable: replicating
readRawin an isolated process for the largest session (45 MB zstd → 128 MB plaintext) peaked at 647 MB RSS, with theBuffer.concat+toString("utf8")step jumping from 296 MB to 647 MB. Materializing two or three such sessions at once is enough to blow through a 2.1 GB default ceiling. - The same path explains search crashes on large logs: stringifying a whole log in one call exceeds V8's string limit and raises
RangeError: Invalid string length— the same family, covered in a separate article.
DSH plugin mitigation and patch status: move big sessions out, keep one instance
The most effective mitigation shrinks the materialization surface: physically move unused large session directories out of sessions/, and make sure only one harness instance is running. From cheapest to most involved:
-
Move large session directories out physically (not merely archive them). Measured: 15 large sessions dropped from ~3 GB heap to ~306 MB with
GET /back to 0.2 s; on Linux, moving 106 archived sessions (385 MB total, including 45/36/35 MB ones) out left the largest remaining session at 5 MB and the service stable.archivedSessionIdsis not a materialization boundary — archived sessions stay on disk and can still be loaded. -
Keep a single writer. Multiple processes writing the same session invite log corruption (the
seq gapfamily), and a corrupt log re-enters the same full-scan path, amplifying a small problem into a site-wide stall; see session corruption troubleshooting. -
Identify the offending session offline first. Community session-doctor tools use their own loopback API, read headers only, and decode on demand, so they never trigger the official history materialization path (for example
dsh-session-surgeon, installed like any other plugin):shdsh plugin --profile web add github:xiaoshenming/dsh-session-surgeonInstalling through DSH Plugin Hub (Settings → Plugin Marketplace) is preferred: a failed install rolls the manifest back instead of leaving the profile broken.
-
Be careful with
--max-old-space-size. Raising the limit from 2 GB to 4 GB makes V8's GC lazier, and a cgroup peak of 7.3 GB was observed — on memory-constrained machines with swap pressure this trades an immediate crash for a higher water mark, not a fix. -
Budget for the client too. Beyond server-side materialization, the conversation view renders everything with no virtualization, so a long heavy session inflates the browser renderer independently (a Safari/WebKit renderer reached 11.27 GB RSS while the session log was only 796 KB; a fresh renderer was 0.55 GB). The same events pressure both ends.
-
Patch status: two community reference implementations exist — a fork branch
fix/session-history-responsiveness(cross-process writer lease, Zstd decode scheduling slice cut from 500 ms to 16 ms, plus revision-scoped caching of deterministic read failures so an unchanged corrupt file stops being rescanned) and a bounded cold-history read patch (pageSurfaceMessages,readHistoryWindow,historyWindowMaxEvents, with the JSONL backend doing a two-pass streaming scan that keeps only the requested page). As of 0.1.2-rc.1, the full cold-startstructuredCloneseed has not shipped in a release.
DSH plugin troubleshooting notes
Separate "freezes on open" from "crashes after a while" — the mitigation overlaps but the evidence differs, and the first instinct should be to hunt for a large session rather than blame networking or ports. Five points to keep in mind when a DeepSeek Harness plugin web server grows without bound:
- Separate "freezes on open" from "crashes after a while": the first is cold-history materialization, the second unbounded accumulation. Watch
GET /latency and CPU for the first, GC logs and heap curves for the second. - Archiving is not isolation: the real boundary is whether the files are inside the
sessions/directory. - Rising RSS is not automatically a leak: the gdb and
malloc_trimevidence shows a large share is allocator high-water mark; track both the V8 heap and RSS. - One bad session can stall everyone: that is the amplifier here, so when the whole UI goes unresponsive, look for a large or corrupt session before suspecting networking or ports.
- Back up before moving anything: confirm no process holds the session, and keep a record of the original paths so you can move it back.

Sources: Discussion #3876, Discussion #1550, Discussion #1859.
FAQ
In a DSH plugin web server, about 50 minutes of use ends in JavaScript heap out of memory, and four unbounded accumulators were located: FrameQueue.buffer in api-proxy.ts has no cap and copies every frame into every subscriber queue; Session.log in core/session is append-only; session-projection-cache marks rows clean on session/disposed but never deletes them (the underlying KvTable stays resident); and write-behind's pending keeps an entire failed batch forever with a structuredClone per event. Both 0.1.0-rc.5 and 0.1.1-rc.2 reproduce it (source: Discussion #3876).
In DeepSeek Harness, a cold session's historySourceFor() calls sessionPersistence.inspect() and receives the complete event array before paginate() runs. maxMessages bounds only the payload sent to the client, not Zstd decompression, JSON parsing, validation, or event materialization — all of which run on the same Node main thread as the web server. One measured 461,981-event session (16.5 MB decoded) made GET / time out while CLOSE_WAIT connections piled up on port 3080 (source: Discussion #1550).
In a DSH plugin, archivedSessionIds is a display-level filter: archived large sessions stay in the sessions/ directory and are still fully materialized. Controlled experiment: 15 large sessions brought boot heap to ~3 GB with GET / timing out; physically moving 11 session directories out of sessions/<cwd>/ brought heap to a stable ~306 MB and GET / back to 0.2 s. Setting preparedSessionCacheSize to 1 does nothing either — it is a retention knob, not a materialization gate (source: Discussion #1550).
The DSH plugin memory issue is not fixed as of 0.1.2-rc.1: packing the published dsh-session-persistence tarball still shows the full seed deep copy, seed: loaded.events.map((event) => structuredClone(event)) (lib/index.js:1390 and :1586), and a Linux user saw five crashes in 20 minutes on 0.1.2-rc.1. Raising --max-old-space-size only trades a crash for a higher peak (a cgroup peak of 7.3 GB was observed); it is not a treatment. Community patches for writer leasing and bounded cold-history reads are in progress (source: Discussion #3876).
Related Terms
- cold history
- A past session that is not in the current process's memory. Opening it requires reading Zstd records from disk, decoding, parsing, validating, and materializing events, with a cost proportional to log size.— https://github.com/deepseek-ai/deepseek-harness/discussions/1550
- backpressure
- Slowing, bounding, or coalescing a producer when the consumer (for example a browser tab) cannot keep up. The publicly discussed FrameQueue has no backpressure, so a slow consumer lets frames accumulate without limit.— https://github.com/deepseek-ai/deepseek-harness/discussions/3876
- allocator high-water mark
- Memory that was freed but not returned to the operating system, so RSS only grows. One measurement showed RSS dropping from 5.48 GB to 549 MB after malloc_trim, meaning much of the growth was a high-water mark rather than a resident leak.— https://github.com/deepseek-ai/deepseek-harness/discussions/3876
Sources
- deepseek-harness Discussion #3876: dsh web memory keeps leaking and OOMs after ~50 minutes (four unbounded accumulators located)· deepseek-ai (GitHub Discussions)
- deepseek-harness Discussion #1550: cold history loading fully materializes large or corrupt logs and can stall the entire web server· deepseek-ai (GitHub Discussions)
- deepseek-harness Discussion #1859: session search crashes with RangeError: Invalid string length on large logs· deepseek-ai (GitHub Discussions)