Back to Blog
DevOps 4 min readJun 1, 2025

Running Multiple WebSocket Servers on the Same Node.js HTTP Server

I hit this today while adding a second WebSocket server to an existing Node.js app. The first server worked fine. The second one silently failed — WebSocket connections refused with no error on the server side. Here's exactly what happened and how I fixed it.

The Setup

I had an existing Node.js Express app with one WebSocket server for real-time streaming on /stream-a. I needed to add a second WebSocket server on a different path /stream-b.

The natural thing to do — create two WebSocketServer instances on the same HTTP server:

javascript
import { WebSocketServer } from 'ws';

const serverA = new WebSocketServer({ server: httpServer, path: '/stream-a' });
const serverB = new WebSocketServer({ server: httpServer, path: '/stream-b' });

The Problem

After adding serverB, the browser showed:

bash
WebSocket connection to 'wss://api.yourdomain.com/stream-b' failed:

The backend showed zero logs — no connection attempt, no error. Server A continued working perfectly. Server B was completely silent.

Running a curl test confirmed the endpoint was reachable:

bash
curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  https://api.yourdomain.com/stream-b

# Returns:
HTTP/1.1 400 Bad Request
Missing or invalid Sec-WebSocket-Key header

The 400 from the ws library meant the endpoint was reachable — but browser WebSocket connections were still failing.

Why It Happens

When you create a WebSocketServer with { server: httpServer }, the ws library attaches its own upgrade event listener to the HTTP server.

The problem: the first WebSocketServer's upgrade listener consumes the upgrade event. When a second server is attached the same way, it never receives the upgrade request because the first listener already handled it — and rejected it since the path didn't match.

javascript
// What ws does internally when you pass { server: httpServer }:
httpServer.on('upgrade', (req, socket, head) => {
  // serverA handles ALL upgrade requests
  // If path is /stream-b → serverA rejects it → serverB never sees it
});
💡

This is a known limitation of the ws library when multiple instances share the same HTTP server. The path option does NOT create separate isolated listeners — it just filters inside one shared listener.

The Fix — noServer: true

The solution is to take manual control of the HTTP upgrade event using noServer: true. Each WebSocket server opts out of auto-attaching to the HTTP server, and you route upgrade requests to the right server yourself.

javascript
import { WebSocketServer } from 'ws';
import { URL } from 'url';

// Both servers opt out of auto-attaching
const serverA = new WebSocketServer({ noServer: true });
const serverB = new WebSocketServer({ noServer: true });

// You handle the HTTP upgrade event once
httpServer.on('upgrade', (request, socket, head) => {
  const { pathname } = new URL(request.url, 'http://localhost');

  if (pathname === '/stream-a') {
    serverA.handleUpgrade(request, socket, head, (ws) => {
      serverA.emit('connection', ws, request);
    });
    return;
  }

  if (pathname === '/stream-b') {
    serverB.handleUpgrade(request, socket, head, (ws) => {
      serverB.emit('connection', ws, request);
    });
    return;
  }

  // Unknown path — destroy the socket
  socket.destroy();
});

Clean Pattern for Multiple Servers

If you have many WebSocket servers, a map-based router is cleaner:

javascript
const wsServers = new Map([
  ['/stream-a', new WebSocketServer({ noServer: true })],
  ['/stream-b', new WebSocketServer({ noServer: true })],
  ['/stream-c', new WebSocketServer({ noServer: true })],
]);

httpServer.on('upgrade', (request, socket, head) => {
  const { pathname } = new URL(request.url, 'http://localhost');
  const wss = wsServers.get(pathname);

  if (!wss) { socket.destroy(); return; }

  wss.handleUpgrade(request, socket, head, (ws) => {
    wss.emit('connection', ws, request);
  });
});

Key Takeaways

  • Never attach two WebSocketServer instances to the same HTTP server using { server: httpServer } — only the first one will actually receive connections.
  • Use { noServer: true } on all instances and handle routing yourself via a single httpServer.on('upgrade', ...) listener.
  • The curl 400 "Missing Sec-WebSocket-Key" test is useful — it confirms the endpoint is reachable even when browser WebSocket fails.
  • If a path doesn't match any WebSocket server, call socket.destroy()to cleanly close the connection instead of leaving it hanging.
All postsNode.js · WebSocket · ws · DevOps

naresh@gowda:~$ built with Next.js + Tailwind + Framer Motion