---
name: fastify-websockets
description: WebSocket patterns with Fastify and @fastify/websocket
---

# Fastify WebSocket Patterns

## Setup

```typescript
import Fastify from 'fastify';
import websocket from '@fastify/websocket';

const app = Fastify({ logger: true });

await app.register(websocket);

app.get('/ws', { websocket: true }, (socket, request) => {
  socket.on('message', (message) => {
    const data = message.toString();
    socket.send(`Echo: ${data}`);
  });

  socket.on('close', () => {
    request.log.info('Client disconnected');
  });

  socket.on('error', (error) => {
    request.log.error({ err: error }, 'WebSocket error');
  });
});

await app.listen({ port: 3000 });
```

## Authentication with Hooks

Use Fastify hooks for WebSocket authentication — they run before the upgrade:

```typescript
app.register(async function wsRoutes(fastify) {
  fastify.addHook('preValidation', async (request, reply) => {
    const token = request.headers.authorization?.replace('Bearer ', '');
    if (!token) {
      reply.code(401).send({ error: 'Unauthorized' });
      return;
    }
    request.user = await verifyToken(token);
  });

  fastify.get('/ws', { websocket: true }, (socket, request) => {
    request.log.info({ userId: request.user.id }, 'WebSocket connected');

    socket.on('message', (message) => {
      // Handle authenticated messages
    });
  });
});
```

## Connection Options

```typescript
app.register(websocket, {
  options: {
    maxPayload: 1048576, // 1MB max message size
    clientTracking: true,
    perMessageDeflate: {
      zlibDeflateOptions: {
        chunkSize: 1024,
        memLevel: 7,
        level: 3,
      },
      threshold: 1024,
    },
  },
});
```

## Broadcast to All Clients

```typescript
const clients = new Set<WebSocket>();

app.get('/ws', { websocket: true }, (socket, request) => {
  clients.add(socket);

  socket.on('close', () => {
    clients.delete(socket);
  });

  socket.on('message', (message) => {
    for (const client of clients) {
      if (client !== socket && client.readyState === WebSocket.OPEN) {
        client.send(message);
      }
    }
  });
});

// Broadcast from HTTP route
app.post('/broadcast', async (request) => {
  const { message } = request.body;

  for (const client of clients) {
    if (client.readyState === WebSocket.OPEN) {
      client.send(JSON.stringify({ type: 'broadcast', message }));
    }
  }

  return { sent: clients.size };
});
```

## Rooms Pattern

```typescript
const rooms = new Map<string, Set<WebSocket>>();

function joinRoom(socket: WebSocket, roomId: string) {
  if (!rooms.has(roomId)) {
    rooms.set(roomId, new Set());
  }
  rooms.get(roomId)!.add(socket);
}

function leaveRoom(socket: WebSocket, roomId: string) {
  rooms.get(roomId)?.delete(socket);
  if (rooms.get(roomId)?.size === 0) {
    rooms.delete(roomId);
  }
}

function broadcastToRoom(roomId: string, message: string, exclude?: WebSocket) {
  const room = rooms.get(roomId);
  if (!room) return;

  for (const client of room) {
    if (client !== exclude && client.readyState === WebSocket.OPEN) {
      client.send(message);
    }
  }
}

app.get('/ws/:roomId', { websocket: true }, (socket, request) => {
  const { roomId } = request.params as { roomId: string };

  joinRoom(socket, roomId);

  socket.on('message', (message) => {
    broadcastToRoom(roomId, message.toString(), socket);
  });

  socket.on('close', () => {
    leaveRoom(socket, roomId);
  });
});
```

## Heartbeat/Ping-Pong

```typescript
const HEARTBEAT_INTERVAL = 30000;
const clients = new Map<WebSocket, { isAlive: boolean }>();

app.get('/ws', { websocket: true }, (socket, request) => {
  clients.set(socket, { isAlive: true });

  socket.on('pong', () => {
    const client = clients.get(socket);
    if (client) client.isAlive = true;
  });

  socket.on('close', () => {
    clients.delete(socket);
  });
});

setInterval(() => {
  for (const [socket, state] of clients) {
    if (!state.isAlive) {
      socket.terminate();
      clients.delete(socket);
      continue;
    }
    state.isAlive = false;
    socket.ping();
  }
}, HEARTBEAT_INTERVAL);
```

## Rate Limiting

```typescript
function createRateLimiter(limit: number, windowMs: number) {
  const limits = new Map<WebSocket, { count: number; resetAt: number }>();

  return (socket: WebSocket): boolean => {
    const now = Date.now();
    let state = limits.get(socket);

    if (!state || now > state.resetAt) {
      state = { count: 0, resetAt: now + windowMs };
      limits.set(socket, state);
    }

    state.count++;
    return state.count <= limit;
  };
}

const messageLimit = createRateLimiter(100, 60000);

app.get('/ws', { websocket: true }, (socket, request) => {
  socket.on('message', (message) => {
    if (!messageLimit(socket)) {
      socket.send(JSON.stringify({ error: 'Rate limit exceeded' }));
      return;
    }
    // Process message
  });

  socket.on('close', () => {
    // Rate limiter map cleanup happens automatically via WeakRef or periodic sweep
  });
});
```

## Structured Message Protocol

```typescript
interface WSMessage {
  type: string;
  payload?: unknown;
  id?: string;
}

app.get('/ws', { websocket: true }, (socket, request) => {
  function send(message: WSMessage) {
    socket.send(JSON.stringify(message));
  }

  socket.on('message', (raw) => {
    let message: WSMessage;

    try {
      message = JSON.parse(raw.toString());
    } catch {
      send({ type: 'error', payload: 'Invalid JSON' });
      return;
    }

    switch (message.type) {
      case 'ping':
        send({ type: 'pong', id: message.id });
        break;
      case 'subscribe':
        handleSubscribe(socket, message.payload);
        send({ type: 'subscribed', payload: message.payload, id: message.id });
        break;
      case 'message':
        handleMessage(socket, message.payload);
        break;
      default:
        send({ type: 'error', payload: 'Unknown message type' });
    }
  });
});
```

## Graceful Shutdown

```typescript
import closeWithGrace from 'close-with-grace';

const connections = new Set<WebSocket>();

app.get('/ws', { websocket: true }, (socket, request) => {
  connections.add(socket);
  socket.on('close', () => connections.delete(socket));
});

closeWithGrace({ delay: 5000 }, async ({ signal }) => {
  for (const socket of connections) {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify({ type: 'shutdown', message: 'Server is shutting down' }));
      socket.close(1001, 'Server shutdown');
    }
  }
  await app.close();
});
```

## Full-Duplex Stream Pattern

```typescript
app.get('/ws/stream', { websocket: true }, async (socket, request) => {
  const stream = createDataStream();

  stream.on('data', (data) => {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(JSON.stringify({ type: 'data', payload: data }));
    }
  });

  stream.on('end', () => {
    socket.send(JSON.stringify({ type: 'end' }));
    socket.close();
  });

  socket.on('message', (message) => {
    const { type } = JSON.parse(message.toString());
    if (type === 'pause') stream.pause();
    else if (type === 'resume') stream.resume();
  });

  socket.on('close', () => {
    stream.destroy();
  });
});
```
