Skip to content

Memory leak: serve and web commands leak child processes on exit; SSE heartbeat interval leaks on server-initiated close #14091

@s2bomb

Description

@s2bomb

Description

The serve and web commands have no graceful shutdown. When the process receives SIGTERM or SIGINT, child resources (MCP subprocesses, LSP servers, file watchers, SQLite connections) are never cleaned up because Instance.disposeAll() is never called.

Separately, the SSE /event endpoint leaks a setInterval heartbeat and a Bus.subscribeAll subscription when the server closes a stream (on InstanceDisposed), because cleanup only runs in onAbort — which Hono only fires on client disconnect, not on stream.close().

Bug 1: Dead server.stop() in serve.ts and web.ts

serve.ts:17-18:

await new Promise(() => {})   // blocks forever — never resolves
await server.stop()           // unreachable

Same pattern in web.ts:78-79. No signal handlers are registered, so SIGTERM/SIGINT use the kernel default (immediate kill, no cleanup).

The correct pattern already exists in worker.ts:137-147:

async shutdown() {
  if (eventStream.abort) eventStream.abort.abort()
  await Promise.race([
    Instance.disposeAll(),
    new Promise((resolve) => { setTimeout(resolve, 5000) }),
  ])
  if (server) server.stop(true)
}

Bug 2: SSE heartbeat interval leaks on server-initiated stream close

server.ts:513-539clearInterval(heartbeat) and unsub() are inside stream.onAbort(). Hono's onAbort only fires on client disconnect (via ReadableStream.cancel()). When the server closes the stream on InstanceDisposed via stream.close(), the onAbort callback never fires.

Each server-closed SSE client leaks:

  • One setInterval (10s heartbeat) writing to a closed stream
  • One Bus.subscribeAll subscription processing every bus event

Steps to reproduce

Bug 1:

bun dev serve --port 14096 &
SERVE_PID=$!
sleep 3
kill -TERM $SERVE_PID
wait $SERVE_PID
echo "exit code: $?"

Exit code is 143 (SIGTERM default), zero shutdown logs, child processes survive. Each orphaned process holds ~200-280MB RSS and they accumulate on every restart.

Bug 2:
Connect an SSE client, trigger InstanceDisposed — heartbeat interval keeps firing after stream.close().

OpenCode version

Current dev branch

Operating System

Linux (also affects macOS — any platform running serve or web)

Metadata

Metadata

Assignees

Labels

coreAnything pertaining to core functionality of the application (opencode server stuff)perfIndicates a performance issue or need for optimization

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions