Skip to content

WebSocket.close() orderly TLS shutdown can pin process exit for many seconds; need terminate()/unref #5570

Closed

Description

@steve02081504

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 the ws package), or
  • unref() 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:

  1. Rely on orderly close and accept multi-second exit delays, or
  2. Reach into undocumented internals / process._getActiveHandles() to destroy()/unref() sockets.

Requested API (either is fine)

  1. websocket.terminate() (or destroy()) — abort immediately, destroy the TCP/TLS socket (same effect as internal failWebsocketConnection without requiring a protocol error), or
  2. 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

  1. steve02081504 commented on Jul 20, 2026

    @steve02081504
    Author

    We hit this in @steve02081504/fount-p2p: Nostr discovery/signaling needs prompt process exit after shutdown, but undici WebSocket.close() can keep TLSSockets alive for several seconds against public wss:// relays.

    Current workaround in fount-p2p: use the ws package and call terminate() on cleanup/abort (see discovery/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 the ws dependency and switch Nostr back to the platform WebSocket.

    Tracking this issue so we know when that cleanup is possible.

  2. mcollina commented on Jul 20, 2026

    @mcollina
    SponsorMember

    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.

  3. KhafraDev commented on Jul 21, 2026

    @KhafraDev
    Member

    just use ws

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions