--- 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>(); 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(); 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]]