NestJS — WebSockets in workers

WebSocket upgrades bypass middleware — app.use never sees them — so the housed proxy gets a second mounting point on the server's 'upgrade' event.

Main thread

main.tsbackend
// src/main.ts — upgrades bypass app.use, so the proxy mounts on the
// server's 'upgrade' event instead:
import { proxyUpgradeToWorker } from '@atolljs/node/http';

const pool = getAtollPool('housed');
const tracker = workerHttpPorts(pool);
let cursor = 0;

app.getHttpServer().on('upgrade',
  proxyUpgradeToWorker({
    pool,
    tracker,
    worker: (workers) => workers[cursor++ % workers.length],
  }),
);

The handshake replays to the resolved worker's internal listener (headers preserved, host rewritten), then the client and worker sockets splice — frames flow end-to-end without the API thread. Unlike proxyToWorker the URL isn't mount-stripped here, so to is a rewrite prefix — usually omitted.

Worker side — a real Nest gateway

Inside the housed app it's an ordinary @WebSocketGateway — attach the ws adapter to the server serveHttp returns and Nest's upgrade handling takes over, exactly as on a single process.

housed/housed-incidents.gateway.tsbackend
// housed/housed-incidents.gateway.ts — a normal ws gateway that exists
// ONLY inside workers. The upgrade handshake replays to the worker's
// internal listener, then frames tunnel socket↔socket.
import { WebSocketGateway, SubscribeMessage } from '@nestjs/websockets';

@WebSocketGateway({ path: '/api/housed/ws' })
export class IncidentsGateway {
  @SubscribeMessage('ping')
  ping() {
    return { event: 'pong', worker: threadId }; // per-worker state works too
  }
}

Clustered listeners

On the clustered port nothing is needed at all — the upgrade rides inside the transferred socket, so the worker's ws adapter handles it in-worker. The plumbing is documented under Backend → Node.js → WebSockets.