Files
2nd/10_Wiki/Topics/Coding/Backend_WebSocket_Production.md
T
2026-05-10 22:08:15 +09:00

7.1 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
backend-websocket-production WebSocket Production — auth / scale / pub/sub Coding draft B conceptual 2026-05-09 2026-05-09
backend
websocket
vibe-coding
language applicable_to
TS
Backend
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)

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)

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)

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)

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

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

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

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

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

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

Compression

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

→ Bandwidth ↓. CPU ↑.

Socket.io (high-level)

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)

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

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

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)

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)

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

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.

🔗 관련 문서