---
id: backend-websocket-production
title: WebSocket Production — auth / scale / pub/sub
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [backend, websocket, vibe-coding]
tech_stack: { language: "TS", applicable_to: ["Backend"] }
applied_in: []
aliases: [WebSocket production, WebSocket auth, sticky session, Redis pub/sub, Socket.io, Pusher, Ably]
---

# WebSocket Production

> Demo WS = simple. **Production = auth / scale / pub-sub / heartbeat / reconnect**. 1 server 가 ~10k connection.

## 📖 핵심 개념
- WebSocket = persistent connection.
- Sticky session 필요 (load balancer).
- Pub/sub 가 multi-instance.
- Heartbeat = idle timeout 방지.

## 💻 코드 패턴

### Basic (Node + ws)
```ts
import { WebSocketServer } from 'ws';

const wss = new WebSocketServer({ port: 8080 });

wss.on('connection', (ws, req) => {
  ws.on('message', (data) => {
    ws.send('echo: ' + data);
  });
});
```

### Auth (JWT)
```ts
wss.on('connection', (ws, req) => {
  const url = new URL(req.url!, `http://${req.headers.host}`);
  const token = url.searchParams.get('token');
  
  try {
    const payload = jwt.verify(token!, secret);
    ws.userId = payload.sub;
  } catch {
    ws.close(1008, 'unauthorized');
    return;
  }
  
  // ... handle
});
```

→ Cookie / header / query param.

### Sticky session (LB)
```nginx
upstream ws {
    ip_hash;
    server ws1:8080;
    server ws2:8080;
}

server {
    location /ws {
        proxy_pass http://ws;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}
```

→ 같은 client = 같은 server.

### Pub/sub (multi-instance)
```ts
import Redis from 'ioredis';

const sub = new Redis();
const pub = new Redis();

// 매 server 가 subscribe
sub.subscribe('chat:room:1');
sub.on('message', (channel, msg) => {
  // 모든 connected client 에 broadcast
  wss.clients.forEach(c => c.send(msg));
});

// Send (다른 server 가 broadcast)
pub.publish('chat:room:1', JSON.stringify({ text: 'hi' }));
```

→ Cross-instance broadcast.

### Heartbeat / ping
```ts
wss.on('connection', (ws) => {
  ws.isAlive = true;
  ws.on('pong', () => { ws.isAlive = true; });
  
  const interval = setInterval(() => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  }, 30000);
  
  ws.on('close', () => clearInterval(interval));
});
```

→ 30s 마다 ping. Pong 안 = terminate.

### Client reconnect
```ts
class WSClient {
  private ws?: WebSocket;
  private retries = 0;
  
  connect() {
    this.ws = new WebSocket(url);
    this.ws.onopen = () => { this.retries = 0; };
    this.ws.onclose = () => this.reconnect();
    this.ws.onerror = () => this.ws?.close();
  }
  
  reconnect() {
    const delay = Math.min(30000, 1000 * Math.pow(2, this.retries++));
    setTimeout(() => this.connect(), delay);
  }
}
```

→ Exponential backoff.

### Subscribe / room
```ts
const rooms = new Map<string, Set<WebSocket>>();

ws.on('message', (data) => {
  const msg = JSON.parse(data.toString());
  if (msg.type === 'join') {
    if (!rooms.has(msg.room)) rooms.set(msg.room, new Set());
    rooms.get(msg.room)!.add(ws);
  } else if (msg.type === 'leave') {
    rooms.get(msg.room)?.delete(ws);
  } else if (msg.type === 'broadcast') {
    rooms.get(msg.room)?.forEach(c => c.send(JSON.stringify(msg)));
  }
});
```

### Backpressure
```ts
ws.send(data, (err) => {
  if (err) console.error(err);
});

// 또는 bufferedAmount check
if (ws.bufferedAmount > 1_000_000) {
  // Slow client — drop / skip.
}
```

### Compression
```ts
const wss = new WebSocketServer({
  port: 8080,
  perMessageDeflate: true,
});
```

→ Bandwidth ↓. CPU ↑.

### Socket.io (high-level)
```ts
import { Server } from 'socket.io';
const io = new Server(server, {
  cors: { origin: '*' },
  adapter: createAdapter(redis, pub),
});

io.on('connection', (socket) => {
  socket.join('room1');
  socket.to('room1').emit('hello');
});
```

→ Built-in: rooms, fallback, reconnect, pub/sub.

### Pusher / Ably (managed)
```ts
import Pusher from 'pusher-js';
const pusher = new Pusher(key, { cluster: 'mt1' });
const channel = pusher.subscribe('chat-1');
channel.bind('message', (data) => console.log(data));
```

→ Self-host 안 하고 싶으면.

### Cloudflare Durable Objects
```ts
// 1 chat room = 1 DO instance.
// Stateful + global.
```

→ [[Backend_Edge_Runtime_Deep]].

### Capacity per instance
```
Node + ws: ~10k connection (4 GB RAM).
uWebSockets.js: ~100k+ (C++ binding).
Go gorilla: ~100k.
Erlang / Elixir Phoenix: 매우 크.

→ 큰 = sticky LB + N instance.
```

### Phoenix Channels (Elixir)
```elixir
defmodule MyApp.RoomChannel do
  use Phoenix.Channel
  
  def join('room:lobby', _, socket) do
    {:ok, socket}
  end
end
```

→ 2M concurrent (single server). Best-in-class.

### Authorization (per message)
```ts
ws.on('message', async (data) => {
  const msg = JSON.parse(data);
  
  // Check permission per action
  if (msg.type === 'admin-action' && !ws.user.isAdmin) {
    ws.send(JSON.stringify({ error: 'forbidden' }));
    return;
  }
  
  // ... handle
});
```

→ Connection auth 가 끝 X. Per-action 도.

### Rate limit (per connection)
```ts
const limits = new Map<WebSocket, { count: number; reset: number }>();

ws.on('message', () => {
  const now = Date.now();
  const limit = limits.get(ws) ?? { count: 0, reset: now + 60000 };
  
  if (now > limit.reset) {
    limit.count = 0;
    limit.reset = now + 60000;
  }
  
  limit.count++;
  if (limit.count > 100) {
    ws.close(1008, 'rate limit');
    return;
  }
});
```

### Monitor
```
- Concurrent connection.
- Messages per sec.
- Disconnect rate.
- Heartbeat fail rate.
- Memory / CPU.

→ Datadog / Prometheus.
```

### Graceful shutdown
```ts
process.on('SIGTERM', async () => {
  // Notify all
  wss.clients.forEach(ws => ws.send(JSON.stringify({ type: 'reconnect-soon' })));
  
  // Wait
  await new Promise(r => setTimeout(r, 5000));
  
  // Close
  wss.close();
});
```

→ Client 가 다른 instance 로 reconnect.

### vs SSE
```
WebSocket: bidirectional.
SSE: server → client only.

→ Chat = WS.
Live update = SSE 또는 WS.
```

### vs HTTP/3 streams
```
HTTP/3 (WebTransport): UDP + multiple stream.
WebSocket: TCP single.

→ Future = WebTransport.
Now = WebSocket.
```

→ [[Web_WebTransport_HID_USB]].

## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| 작은 chat | ws (Node) |
| Production scale | Socket.io + Redis adapter |
| Managed | Pusher / Ably |
| Edge | Cloudflare DO |
| 매우 큰 | Phoenix / uWebSockets |
| Streaming server → client | SSE |

## ❌ 안티패턴
- **No sticky LB**: round-robin 가 connection 깨짐.
- **No pub/sub**: cross-instance broadcast X.
- **No heartbeat**: idle timeout.
- **No rate limit**: spam.
- **Connection auth 만**: per-action 도.
- **No reconnect**: bad UX.
- **Compression always**: CPU.

## 🤖 LLM 활용 힌트
- Sticky LB + Redis pub/sub 가 multi-instance.
- Heartbeat 30s.
- Client reconnect (exponential).
- Phoenix / uWebSockets 가 큰 scale.

## 🔗 관련 문서
- [[Backend_WebSocket_Scaling]]
- [[Web_WebSocket_Reconnect]]
- [[Backend_NATS_JetStream]]
