Description
opencode-cli.exe memory consumption grows up to 24.5 GB over time. The root cause is SSE connections that get stuck in TCP CLOSE_WAIT state — the server never detects client disconnection, so cleanup never runs and unbounded AsyncQueue buffers grow indefinitely.
Evidence:
-
CLOSE_WAIT zombie connections: Measured 66-74 TCP connections stuck in CLOSE_WAIT on the server port, while only 12 or fewer are ESTABLISHED. CLOSE_WAIT means the client sent FIN (disconnected) but the server never closed its socket.
-
Memory growth rate: Measured +434 MB in 30 seconds (14.5 MB/sec) with a stable count of zombie connections — the existing zombie queues are continuously filling up.
-
Timeline of measurements:
- Start: RAM 0.96 GB, 66 CLOSE_WAIT
- 30 min later: RAM 2.5 GB, 66 CLOSE_WAIT
- 1 hour later: RAM 3.69 GB, 70 CLOSE_WAIT
- Peak observed: RAM 24.5 GB, 74 CLOSE_WAIT (from earlier session)
-
Duplicate MCP child processes: 7× docker.exe, 10× mcp-lsp-bridge.exe, 7× cmd.exe (playwright) — should be 1 each per project. These accumulate across instance re-initializations.
-
Code root cause (src/server/instance/event.ts:71): stream.onAbort(stop) is the ONLY way cleanup runs (clearing heartbeat interval, unsubscribing from Bus events, terminating AsyncQueue). But onAbort is never called when the TCP connection enters CLOSE_WAIT in Bun/Hono — the server-side socket remains open, the SSE stream handler keeps running, and the AsyncQueue (src/util/queue.ts) has no size limit and no backpressure.
-
Same pattern in src/server/instance/global.ts — the streamEvents() helper is used for /event, /global/event, and /global/sync-event endpoints, all with the same unbounded AsyncQueue + onAbort-only cleanup.
What happens per zombie connection:
setInterval pushes heartbeat JSON every 10 seconds → never cleared
Bus.subscribeAll() pushes ALL server events → never unsubscribed
AsyncQueue grows without bound → never terminated
- With 66+ zombie connections, that's 66× the event stream buffered with no consumer
Suggested fixes:
- Add size limit/backpressure to
AsyncQueue (src/util/queue.ts) — drop oldest events or terminate subscription when queue exceeds threshold
- Add idle timeout on SSE connections — if
stream.writeSSE() hasn't succeeded in N seconds, call stop() forcefully
- Detect write failures on the socket — if
writeSSE throws or socket is in half-closed state, trigger cleanup
- Investigate why Hono's
stream.onAbort doesn't fire on TCP half-close in Bun runtime
- Fix MCP child process duplication — ensure previous processes are killed before spawning new ones
- Enable
OPENCODE_AUTO_HEAP_SNAPSHOT in Desktop Electron for automated diagnostics when RSS > 2GB
Plugins
Docker MCP gateway, mcp-lsp-bridge, Playwright MCP, mcp-server-qdrant
OpenCode version
OpenCode Desktop v1.4.3
Steps to reproduce
- Launch OpenCode Desktop
- Open a project and actively use it (chat, tool calls, LSP)
- Minimize/restore the window several times (triggers SSE reconnection in the Electron renderer)
- Monitor with PowerShell:
$id = (Get-Process -Name "opencode-cli").Id
$conns = netstat -ano | Select-String "$id"
$cw = ($conns | Select-String "CLOSE_WAIT").Count
$es = ($conns | Select-String "ESTABLISHED").Count
$ram = [math]::Round((Get-Process -Id $id).WorkingSet64/1GB,2)
Write-Host "RAM: ${ram}GB | Alive: $es | Zombie: $cw"
- Observe CLOSE_WAIT count growing alongside RAM — memory grows ~14 MB/sec even with stable zombie count
- Over hours of use, RAM reaches 24.5 GB
Screenshot and/or share link

Operating System
windows 11
Terminal
No response
Description
opencode-cli.exememory consumption grows up to 24.5 GB over time. The root cause is SSE connections that get stuck in TCP CLOSE_WAIT state — the server never detects client disconnection, so cleanup never runs and unboundedAsyncQueuebuffers grow indefinitely.Evidence:
CLOSE_WAIT zombie connections: Measured 66-74 TCP connections stuck in CLOSE_WAIT on the server port, while only 12 or fewer are ESTABLISHED. CLOSE_WAIT means the client sent FIN (disconnected) but the server never closed its socket.
Memory growth rate: Measured +434 MB in 30 seconds (14.5 MB/sec) with a stable count of zombie connections — the existing zombie queues are continuously filling up.
Timeline of measurements:
Duplicate MCP child processes: 7× docker.exe, 10× mcp-lsp-bridge.exe, 7× cmd.exe (playwright) — should be 1 each per project. These accumulate across instance re-initializations.
Code root cause (
src/server/instance/event.ts:71):stream.onAbort(stop)is the ONLY way cleanup runs (clearing heartbeat interval, unsubscribing from Bus events, terminating AsyncQueue). ButonAbortis never called when the TCP connection enters CLOSE_WAIT in Bun/Hono — the server-side socket remains open, the SSE stream handler keeps running, and theAsyncQueue(src/util/queue.ts) has no size limit and no backpressure.Same pattern in
src/server/instance/global.ts— thestreamEvents()helper is used for/event,/global/event, and/global/sync-eventendpoints, all with the same unbounded AsyncQueue + onAbort-only cleanup.What happens per zombie connection:
setIntervalpushes heartbeat JSON every 10 seconds → never clearedBus.subscribeAll()pushes ALL server events → never unsubscribedAsyncQueuegrows without bound → never terminatedSuggested fixes:
AsyncQueue(src/util/queue.ts) — drop oldest events or terminate subscription when queue exceeds thresholdstream.writeSSE()hasn't succeeded in N seconds, callstop()forcefullywriteSSEthrows or socket is in half-closed state, trigger cleanupstream.onAbortdoesn't fire on TCP half-close in Bun runtimeOPENCODE_AUTO_HEAP_SNAPSHOTin Desktop Electron for automated diagnostics when RSS > 2GBPlugins
Docker MCP gateway, mcp-lsp-bridge, Playwright MCP, mcp-server-qdrant
OpenCode version
OpenCode Desktop v1.4.3
Steps to reproduce
Screenshot and/or share link
Operating System
windows 11
Terminal
No response