WebSocket.close() orderly TLS shutdown can pin process exit for many seconds; need terminate()/unref #5570
Description
Bug / Feature
Node's global WebSocket (undici) has no way to force-drop an established connection. Calling ws.close() performs an orderly WebSocket + TLS close. When the peer is a public relay that is slow or ignores the close frame, the underlying TLSSocket keeps the Node event loop alive for several seconds (observed ~7s against common Nostr wss:// relays).
There is also no public way to:
terminate()/ destroy the underlying socket (as in thewspackage), orunref()the connection so background discovery sockets do not pin process lifetime.
Internally, undici already destroys the socket in failWebsocketConnection(), but that path is not exposed to users who simply want to tear down a live socket on application shutdown.
Reproduction
const ws = new WebSocket('wss://nos.lol') // or any reachable wss relay
ws.addEventListener('open', () => {
ws.close()
// Process often stays alive ~5–10s while TLSSocket drains / waits for peer close.
// process._getActiveHandles() still lists TLSSocket after close().
})Contrast: if the handshake never succeeds (ws://127.0.0.1:1), close()/error does not pin the process.
Why it matters
Long-running apps that use WebSocket for best-effort discovery/signaling need shutdown to release the event loop promptly after closing listen sockets. Without terminate() or unref(), the only options are:
- Rely on orderly close and accept multi-second exit delays, or
- Reach into undocumented internals /
process._getActiveHandles()todestroy()/unref()sockets.
Requested API (either is fine)
websocket.terminate()(ordestroy()) — abort immediately, destroy the TCP/TLS socket (same effect as internalfailWebsocketConnectionwithout requiring a protocol error), or- Expose the underlying socket (or
unref()) so callers can mark discovery connections as non-keeping-alive.
Related: #4405 (process does not exit after aborting a request to an unresponsive host).
Environment
- Node.js 26.5.0 (bundled undici 8.7.0)
- Windows 10
Activity
steve02081504 commentedon Jul 20, 2026
We hit this in @steve02081504/fount-p2p: Nostr discovery/signaling needs prompt process exit after
shutdown, but undiciWebSocket.close()can keepTLSSockets alive for several seconds against publicwss://relays.Current workaround in fount-p2p: use the
wspackage and callterminate()on cleanup/abort (seediscovery/nostr.mjs).Follow-up for us: once Node/undici exposes
terminate()(or equivalent force-drop /unref), and Deno's WebSocket supports the same, we can drop thewsdependency and switch Nostr back to the platform WebSocket.Tracking this issue so we know when that cleanup is possible.
mcollina commentedon Jul 20, 2026
The (experimental) Node mechanism for this is https://nodejs.org/docs/latest/api/process.html#processunrefmayberefable. I'll try to see if we can implement this.
KhafraDev commentedon Jul 21, 2026
just use ws