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
// 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.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.