Repository navigation
Windows: Ghost port bindings when server killed via SIGTERM (affects plugins like openralph) #9859
Description
Summary
The opencode serve command lacks signal handlers for graceful shutdown, causing ghost port bindings on Windows when the process is terminated by external tools. This affects plugins like openralph and opencode-ralph that spawn OpenCode as a subprocess and send SIGTERM when exiting.
Problem
When these plugins terminate, they send SIGTERM to the OpenCode server process. However, opencode serve has no signal handlers to catch this and call server.stop(), so:
- The process dies immediately without closing the TCP listener
- On Windows, the port binding persists as a "ghost" - bound to a non-existent PID
- Restarting the server fails with
Failed to start server on port XXXX - The only workarounds are: wait 2-5 minutes, use a different port, or reboot
Observed Behavior
$ opencode serve --port 4190
Error: Failed to start server on port 4190
$ netstat -ano | find "4190"
TCP 127.0.0.1:4190 0.0.0.0:0 LISTENING 464972
$ tasklist /FI "PID eq 464972"
INFO: No tasks are running which match the specified criteria.
The port is bound to a PID that no longer exists - a ghost binding that Windows won't release.
Root Cause
File: packages/opencode/src/cli/cmd/serve.ts
export async function serveCmd(args: Arguments<ServeOptions>) {
const server = await Server.listen({
port: args.port,
hostname: args.hostname,
})
logger.info(`Server started at http://${args.hostname}:${args.port}`)
await new Promise(() => {}) // Waits forever
server.stop() // Never called
}There are no handlers for SIGINT, SIGTERM, or SIGBREAK. When a plugin sends SIGTERM, the process exits immediately without calling server.stop().
Proposed Fix
Add signal handlers to ensure graceful shutdown:
export async function serveCmd(args: Arguments<ServeOptions>) {
const server = await Server.listen({
port: args.port,
hostname: args.hostname,
})
logger.info(`Server started at http://${args.hostname}:${args.port}`)
const shutdown = (signal: string) => {
logger.info(`Received ${signal}, shutting down...`)
server.stop()
process.exit(0)
}
process.on('SIGINT', () => shutdown('SIGINT'))
process.on('SIGTERM', () => shutdown('SIGTERM'))
process.on('SIGBREAK', () => shutdown('SIGBREAK')) // Windows Ctrl+Break
await new Promise(() => {})
}Additionally, enabling reusePort: true in Bun.serve() would allow immediate port reuse even after abnormal termination, providing defense-in-depth.
Environment
- OS: Windows 11
- OpenCode Version: 1.1.29
- Affected Plugins:
Reproduction Steps
- Use openralph or opencode-ralph to spawn
opencode serve --port 4190 - Exit the plugin (which sends SIGTERM to opencode)
- Restart the plugin
- Observe:
Failed to start server on port 4190
Impact
- Severity: High for Windows users
- User Experience: Very poor - requires system restart in worst case
- Affects all plugins/tools that spawn OpenCode as a subprocess
Activity
JosXa commentedon Jan 21, 2026
yo opencode, can you propose a fix in a PR?
JosXa commentedon Jan 21, 2026
Summary
I've implemented the fix for the ghost port bindings issue on Windows.
Changes
packages/opencode/src/cli/cmd/serve.ts- Added signal handlers forSIGINT,SIGTERM, andSIGBREAK(Windows Ctrl+Break) that gracefully callserver.stop()before exitingpackages/opencode/src/server/server.ts- AddedreusePort: truetoBun.serve()for defense-in-depth, allowing immediate port reuse even after abnormal terminationPull Request
Created PR #9866: fix: add signal handlers for graceful shutdown in serve command
github-actions commentedon Mar 28, 2026
To stay organized issues are automatically closed after 90 days of no activity. If the issue is still relevant please open a new one.