websockets/ws · critical · Error

server.handleUpgrade() was called more than once with the…

Error message

server.handleUpgrade() was called more than once with the same socket, possibly due to a misconfiguration

What it means

Thrown by WebSocketServer.completeUpgrade() at websocket-server.js:378-383 when the same network socket already has a WebSocket associated with it (socket[kWebSocket] is set). This symbol is assigned during setSocket() at websocket.js:233, so seeing it means completeUpgrade already ran for this socket. The library treats a second upgrade on one socket as a configuration error, not a normal event.

Solutions

  1. Ensure only ONE WebSocketServer (or one manual handleUpgrade call) processes each upgrade request. Use options.path to route by URL, or use noServer mode with explicit routing in your own 'upgrade' handler.
  2. If you need multiple WS endpoints on one HTTP server, use noServer: true and dispatch based on req.url in a single 'upgrade' listener: if (req.url === '/a') wssA.handleUpgrade(...); else wssB.handleUpgrade(...).
  3. Remove any duplicate 'upgrade' listener you added manually if the server already auto-attaches one (server/noServer modes do this in the constructor at websocket-server.js:125-131).

Example fix

// before — two servers on one HTTP server both grab every upgrade
const wssA = new WebSocketServer({ server: httpServer, path: '/a' });
const wssB = new WebSocketServer({ server: httpServer, path: '/b' });
// (path helps, but safest is noServer routing)

// after — single noServer instance with manual routing
const wssA = new WebSocketServer({ noServer: true });
const wssB = new WebSocketServer({ noServer: true });
httpServer.on('upgrade', (req, socket, head) => {
  const { pathname } = new URL(req.url, 'http://x');
  if (pathname === '/a') wssA.handleUpgrade(req, socket, head, cb);
  else if (pathname === '/b') wssB.handleUpgrade(req, socket, head, cb);
  else socket.destroy();
});
Defensive patterns

Strategy: validation

Validate before calling

// Use noServer mode with explicit routing to avoid double-dispatch
function attachWebSocketServers(httpServer, servers) {
  httpServer.on('upgrade', (req, socket, head) => {
    const { pathname } = new URL(req.url, 'http://placeholder');
    const wss = servers.find(s => s.options.path === pathname);
    if (wss) {
      wss.handleUpgrade(req, socket, head, (ws) => { /* ... */ });
    } else {
      socket.destroy();
    }
  });
}

Type guard

function isSocketAlreadyUpgraded(socket) {
  return socket[kWebSocket] !== undefined;
}

Try / catch

try {
  wss.handleUpgrade(req, socket, head, cb);
} catch (err) {
  if (/more than once with the same socket/.test(err.message)) {
    // a prior handler already upgraded this socket; just destroy to be safe
    socket.destroy();
  } else throw err;
}

Prevention

When it happens

Trigger: Two WebSocketServer instances both attach their 'upgrade' listener to the same httpServer and both call handleUpgrade for the same request; a user manually calls wss.handleUpgrade() in their own 'upgrade' listener while the server also has its auto-attached listener; an 'upgrade' event fires twice on the same socket due to a proxy or framework quirk.

Common situations: Mounting multiple WebSocketServer instances on one HTTP server without a path filter (options.path) or without noServer routing; a framework (e.g. Express/Next/Fastify adapter) that adds its own upgrade handler that double-dispatches; accidentally calling handleUpgrade inside both verifyClient's callback and the default path.

Related errors


AI-assisted analysis of websockets/ws@c791e707ea (2026-08-06). Data as JSON: /api/errors/7a13157ac0069a3d. Report an issue: GitHub.

Appendix: source

Thrown at lib/websocket-server.js:379

   *
   * @param {Object} extensions The accepted extensions
   * @param {String} key The value of the `Sec-WebSocket-Key` header
   * @param {Set} protocols The subprotocols
   * @param {http.IncomingMessage} req The request object
   * @param {Duplex} socket The network socket between the server and client
   * @param {Buffer} head The first packet of the upgraded stream
   * @param {Function} cb Callback
   * @throws {Error} If called more than once with the same socket
   * @private
   */
  completeUpgrade(extensions, key, protocols, req, socket, head, cb) {
    //
    // Destroy the socket if the client has already sent a FIN packet.
    //
    if (!socket.readable || !socket.writable) return socket.destroy();

    if (socket[kWebSocket]) {
      throw new Error(
        'server.handleUpgrade() was called more than once with the same ' +
          'socket, possibly due to a misconfiguration'
      );
    }

    if (this._state > RUNNING) return abortHandshake(socket, 503);

    const digest = createHash('sha1')
      .update(key + GUID)
      .digest('base64');

    const headers = [
      'HTTP/1.1 101 Switching Protocols',
      'Upgrade: websocket',
      'Connection: Upgrade',
      `Sec-WebSocket-Accept: ${digest}`
    ];

View on GitHub (pinned to c791e707ea)