WebSockets in Koa

Add Bidirectional Real-Time Messaging to a Koa Server

WebSockets in Koa

Attach a ws WebSocket server to Koa's underlying http.Server via app.callback(), handle upgrade events, and implement a simple broadcast room pattern for group messaging.

5 min read Level 3/5 #koa#data#websockets
What you'll learn
  • Attach the ws library to Koa's http.Server without third-party wrappers
  • Handle WebSocket connections, messages, and disconnections in Koa
  • Implement a room-based broadcast so messages reach only relevant clients

WebSockets provide a persistent, bidirectional channel between client and server. Unlike SSE, the client can also send messages — making WebSockets the right choice for chat, collaborative editing, and live game state.

Attaching ws to Koa

The ws package is framework-agnostic. Retrieve Koa’s native http.Server via app.callback() and pass it to the WebSocket server.

npm install ws
import Koa from 'koa';
import http from 'node:http';
import { WebSocketServer } from 'ws';
import router from './routes/index.js';

const app = new Koa();
app.use(router.routes()).use(router.allowedMethods());

const server = http.createServer(app.callback());
const wss    = new WebSocketServer({ server });

wss.on('connection', (ws, req) => {
  console.log('Client connected from', req.socket.remoteAddress);

  ws.on('message', (raw) => {
    const msg = JSON.parse(raw.toString());
    handleMessage(ws, msg);
  });

  ws.on('close', () => {
    console.log('Client disconnected');
    leaveAllRooms(ws);
  });

  ws.on('error', (err) => console.error('WS error:', err));
});

server.listen(3000, () => console.log('Listening on :3000'));

Room-Based Broadcast

Rooms are just Set instances keyed by room name. When a message arrives, broadcast it only to clients in the same room.

// rooms.js
const rooms = new Map();   // roomId -> Set<WebSocket>

export function joinRoom(roomId, ws) {
  if (!rooms.has(roomId)) rooms.set(roomId, new Set());
  rooms.get(roomId).add(ws);
}

export function leaveAllRooms(ws) {
  for (const members of rooms.values()) members.delete(ws);
}

export function broadcast(roomId, payload, sender) {
  const members = rooms.get(roomId);
  if (!members) return;
  const data = JSON.stringify(payload);
  for (const client of members) {
    if (client !== sender && client.readyState === 1 /* OPEN */) {
      client.send(data);
    }
  }
}
// handleMessage.js
import { joinRoom, broadcast } from './rooms.js';

export function handleMessage(ws, msg) {
  if (msg.type === 'join') {
    joinRoom(msg.room, ws);
    ws.send(JSON.stringify({ type: 'joined', room: msg.room }));
    return;
  }

  if (msg.type === 'chat') {
    broadcast(msg.room, { type: 'chat', text: msg.text, from: msg.from }, ws);
    return;
  }
}

Heartbeat (Ping/Pong)

Proxies and load balancers close idle connections. Send a ping every 30 seconds and terminate any connection that does not respond.

wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
});

setInterval(() => {
  for (const ws of wss.clients) {
    if (!ws.isAlive) { ws.terminate(); continue; }
    ws.isAlive = false;
    ws.ping();
  }
}, 30_000);

koa-websocket (Alternative)

koa-websocket wraps the above pattern with Koa-style middleware routing if you prefer to keep WebSocket handlers alongside REST routes. It exposes app.ws.use() and works with @koa/router, though the plain ws approach above is often simpler and requires fewer dependencies.

Up Next

Explore GraphQL over Koa using graphql-http for a type-safe query layer on top of your data services.

GraphQL with Koa →