Skip to content

opencode web becomes unresponsive after hours: SSE connection leak causes 7GB+ memory growth and event loop freeze #17628

Closed

Description

@keh4l

Description

Running opencode web on a Linux server (Debian, 56 cores, 125GB RAM), the HTTP server becomes completely unresponsive after a variable period (sometimes minutes, sometimes days). The process remains alive but refuses all HTTP requests, including /global/health. PM2 does not detect the failure because the process never exits.

This is related to #15645, #14091, and #15149, but provides production-level diagnostic data showing the full failure cascade.

Root Cause Analysis

Three bugs in the SSE implementation (server.ts:520-554 and routes/global.ts:71-107) create a fatal chain reaction:

Bug 1: Heartbeat writes are fire-and-forget (server.ts:537-543)

const heartbeat = setInterval(() => {
  stream.writeSSE({  // ❌ NOT awaited, no try/catch
    data: JSON.stringify({ type: "server.heartbeat", properties: {} }),
  })
}, 10_000)

When a client disconnects but onAbort hasn't fired, this interval keeps writing to a dead stream every 10 seconds forever. Errors are silently swallowed.

Bug 2: Bus event writes to dead connections block all event publishing (server.ts:527-533)

const unsub = Bus.subscribeAll(async (event) => {
  await stream.writeSSE({ data: JSON.stringify(event) })  // ❌ hangs on dead connection
})

Bus.publish() calls Promise.all(pending) across all subscribers. If one subscriber's stream.writeSSE() hangs on a dead connection, it blocks event delivery to every other subscriber.

Bug 3: onAbort never fires for CLOSE-WAIT connections

When TCP enters CLOSE-WAIT (client sent FIN, server hasn't acknowledged), Hono/Bun's stream.onAbort() may never trigger. This means:

  • clearInterval(heartbeat) never runs → heartbeat interval leaks
  • unsub() never runs → Bus subscription leaks
  • The Promise at line 546 never resolves → stream handler stays alive forever

Bonus: Proxy to app.opencode.ai has no timeout (server.ts:560)

const response = await proxy(`https://app.opencode.ai${path}`, { ... })
// ❌ No AbortSignal, no timeout — can hang indefinitely

Production Diagnostic Data (captured during freeze)

Memory growth

Metric At startup At freeze (~45 min)
Memory 528 MB (0.4%) 7,134 MB (5.4%)
CPU 49% 33%
TCP connections 4 200+
CLOSE-WAIT connections 0 30+

TCP connection state (abridged)

# All ESTAB connections from port 4069 have Send-Q of 798-800 bytes (write buffer full)
ESTAB  799  0  127.0.0.1:4069  127.0.0.1:56298
ESTAB  799  0  127.0.0.1:4069  127.0.0.1:53070
ESTAB  799  0  127.0.0.1:4069  127.0.0.1:40474
...

# CLOSE-WAIT connections from external client — server never closed its side
CLOSE-WAIT  593  0  38.97.xx.5:4069  13.193.97.167:56050
CLOSE-WAIT  593  0  38.97.xx.5:4069  13.193.97.167:63601
CLOSE-WAIT  593  0  38.97.xx.5:4069  13.193.97.167:63574
... (30+ more)

Key observations

  • curl -m 5 http://localhost:4069/global/health → timeout (event loop frozen)
  • Process still alive in ps aux (PID exists, 33% CPU, 7.1GB RSS)
  • dmesg shows no OOM kill
  • Application log ends cleanly with normal health check responses — no error, no crash

Steps to Reproduce

  1. Run opencode web --port 4069 --hostname 0.0.0.0 on a Linux server
  2. Access the web UI from a browser and use it normally
  3. Occasionally close/reopen browser tabs (creates SSE reconnections)
  4. Wait (variable: minutes to days)
  5. HTTP server becomes completely unresponsive while process stays alive

Suggested Fix

// 1. Add try/catch + await to heartbeat writes
const heartbeat = setInterval(async () => {
  try {
    await stream.writeSSE({ data: JSON.stringify({ type: "server.heartbeat", properties: {} }) })
  } catch {
    clearInterval(heartbeat)
    unsub()
    stream.close()
  }
}, 10_000)

// 2. Add try/catch to bus event writes
const unsub = Bus.subscribeAll(async (event) => {
  try {
    await stream.writeSSE({ data: JSON.stringify(event) })
  } catch {
    clearInterval(heartbeat)
    unsub()
    stream.close()
  }
})

// 3. Add timeout to proxy
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 10_000)
const response = await proxy(`https://app.opencode.ai${path}`, {
  ...c.req,
  signal: controller.signal,
  headers: { ...c.req.raw.headers, host: "app.opencode.ai" },
})
clearTimeout(timeout)

Environment

  • OS: Debian 12 (6.1.0-43-amd64)
  • Server: 56 cores, 125GB RAM
  • OpenCode version: Latest from npm (opencode-ai)
  • Node: v24.12.0
  • Runtime: Bun (embedded in opencode)
  • Setup: nohup opencode web / PM2 managed, accessed via Nginx reverse proxy

Activity

  1. added
    coreAnything pertaining to core functionality of the application (opencode server stuff)
    perfIndicates a performance issue or need for optimization
    webRelates to opencode on web / desktop
    on Mar 15, 2026
  2. github-actions commented on Mar 15, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    These issues appear to describe the same root cause and failure mode. The maintainers may want to consolidate or track this as additional production evidence on one of those existing issues.

  3. jnorthrup commented on Mar 21, 2026

    @jnorthrup

    🚨 URGENT: Cross-Project Memory Leak Crisis - Hardware Damage Risk

    We investigated memory leaks across WebKit-based runtimes (Bun, Claude Code, OpenCode) and found a critical pattern affecting all embedders.

    Hardware Risk Statement

    On Apple Silicon Macs:

    • Memory exhaustion → kernel panic → forced power cycle
    • SOC (System on Chip) is non-replaceable ($500-1500 hardware loss)
    • Sustained memory pressure can cause thermal damage

    On Linux servers:

    • OOM kills → data corruption from unclean shutdowns
    • Kernel soft lockups (356s+) → system death requiring hard reset
    • Database corruption from abrupt termination

    Our Investigation Findings

    Bun Runtime (WebKit JSC):

    • 4 unique JSC GC memory leaks identified
    • JSC::SlotVisitor::drainFromShared race condition (7,214 frames)
    • WTF::HashTable::removeIterator (7,214 frames)
    • WTF::Vector<>::operator= (7,220 frames)
    • JSC::ASTBuilder::createForOfLoop (713 frames)

    OpenCode (from your issues):

    Common Root Cause

    WebKit JavaScriptCore parallel GC marking phase:

    • Multiple threads drain shared work queues
    • Race condition in SlotVisitor::drainFromShared
    • Iterator invalidation during concurrent access
    • Objects marked but not properly tracked → leak

    Upstream References

    Issue Project Status
    WPEWebKit #1622 SlotVisitor::drain crash Open
    WebKit #200863 SlotVisitor::visitChildren crash Open
    LLVM #115992 LSAN false positives Open
    Bun #28343 JSC GC tracking Open

    Tool We Built

    We created test/asan_tracker.zig (760 lines) that:

    • Parses ASAN/LSAN logs automatically
    • Classifies leaks by component (JSC, WTF, Bun, System)
    • Auto-symbolicates with source locations
    • Exports JSON for CI monitoring

    Happy to share this tool with OpenCode maintainers.

    Urgency: Auto-Close Bot Pattern

    We observed issues #17837 and #17185 were closed by github-actions bot within 2 hours for "contributing guidelines" - despite containing:

    • Detailed technical analysis
    • Reproduction steps
    • Root cause identification
    • PR with fixes

    This pattern is dangerous for memory safety issues:

    1. High-quality bug reports dismissed before maintainer review
    2. Hardware damage risk ignored
    3. Cross-project coordination blocked

    Recommended Actions

    Immediate:

    1. ⚠️ Disable auto-close for perf and bug labels
    2. ⚠️ Extend contribution window from 2h to 48h for complex bugs
    3. ⚠️ Require maintainer review before closing memory issues

    Short-term:

    1. Merge PR fix: multiple memory leaks in long-running sessions #10913 (4 leak fixes)
    2. Add RSS monitoring to CI
    3. Implement graceful degradation on memory pressure

    Long-term:

    1. Coordinate with WebKit upstream on GC fixes
    2. Share ASAN tracker tool across embedders
    3. Establish memory safety triage process

    Cross-Project Coordination

    We're coordinating with:

    Memory leaks don't respect project boundaries - this is an ecosystem-wide crisis.


    Evidence:

    Please prioritize this over formatting requirements.

  4. jnorthrup commented on Mar 21, 2026

    @jnorthrup

    🚨 URGENT: Cross-Project Memory Leak Crisis - Hardware Damage Risk

    We investigated memory leaks across WebKit-based runtimes (Bun, Claude Code, OpenCode) and found a critical pattern affecting all embedders.

    Hardware Risk Statement

    On Apple Silicon Macs:

    • Memory exhaustion → kernel panic → forced power cycle
    • SOC (System on Chip) is non-replaceable ($500-1500 hardware loss)
    • Sustained memory pressure can cause thermal damage

    On Linux servers:

    • OOM kills → data corruption from unclean shutdowns
    • Kernel soft lockups (356s+) → system death requiring hard reset
    • Database corruption from abrupt termination

    Our Investigation Findings

    Bun Runtime (WebKit JSC):

    • 4 unique JSC GC memory leaks identified via ASAN tracker
    • JSC::SlotVisitor::drainFromShared race condition (7,214 frames)
    • WTF::HashTable::removeIterator (7,214 frames)
    • WTF::Vector<>::operator= (7,220 frames)
    • JSC::ASTBuilder::createForOfLoop (713 frames)

    OpenCode (from your issues):

    Common Root Cause

    WebKit JavaScriptCore parallel GC marking phase:

    • Multiple threads drain shared work queues
    • Race condition in SlotVisitor::drainFromShared
    • Iterator invalidation during concurrent access
    • Objects marked but not properly tracked → leak

    Upstream References

    Issue Project Status
    WPEWebKit #1622 SlotVisitor::drain crash Open
    WebKit #200863 SlotVisitor::visitChildren crash Open
    LLVM #115992 LSAN false positives Open

    Tool We Built

    We created an ASAN tracker (760 lines of Zig) that:

    • Parses ASAN/LSAN logs automatically
    • Classifies leaks by component (JSC, WTF, Bun, System)
    • Auto-symbolicates with source locations
    • Exports JSON for CI monitoring

    Happy to share this tool with OpenCode maintainers.

    Urgency: Auto-Close Bot Pattern

    We observed issues #17837 and #17185 were closed by github-actions bot within 2 hours for "contributing guidelines" - despite containing:

    • Detailed technical analysis
    • Reproduction steps
    • Root cause identification
    • PR with fixes

    This pattern is dangerous for memory safety issues:

    1. High-quality bug reports dismissed before maintainer review
    2. Hardware damage risk ignored
    3. Cross-project coordination blocked

    Recommended Actions

    Immediate:

    1. ⚠️ Disable auto-close for perf and bug labels
    2. ⚠️ Extend contribution window from 2h to 48h for complex bugs
    3. ⚠️ Require maintainer review before closing memory issues

    Short-term:

    1. Merge PR fix: multiple memory leaks in long-running sessions #10913 (4 leak fixes)
    2. Add RSS monitoring to CI
    3. Implement graceful degradation on memory pressure

    Long-term:

    1. Coordinate with WebKit upstream on GC fixes
    2. Share ASAN tracker tool across embedders
    3. Establish memory safety triage process

    Cross-Project Coordination

    We're coordinating with:

    Memory leaks don't respect project boundaries - this is an ecosystem-wide crisis.


    Evidence:

    Please prioritize this over formatting requirements.

  5. thdxr commented on Apr 2, 2026

    @thdxr
    Member

    Closing in favor of #20695 where we're collecting all memory-related problems. Please follow that issue for updates.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)perfIndicates a performance issue or need for optimizationwebRelates to opencode on web / desktop

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions