Skip to content

Latest commit

 

History

History
837 lines (565 loc) · 24.6 KB

File metadata and controls

837 lines (565 loc) · 24.6 KB

WebSockets and Real-Time Communication Interview Questions

A complete pedagogical guide to WebSockets and real-time communication. Each question explains the concept in plain language and pairs it with a working code example.


How to Use This Guide

Code examples use Node.js with the ws library and socket.io, plus browser JavaScript on the client side. To run the server samples:

npm install ws socket.io express

1. Real-Time Transport Mechanisms

Q1. What are the options for real-time communication over HTTP/TCP?

Answer:

There are four main techniques:

Technique How it works
Short polling Client repeatedly asks "anything new?"
Long polling Server holds the request until data is ready
Server-Sent Events (SSE) One-way HTTP stream from server to client
WebSocket Full-duplex TCP connection upgraded from HTTP

WebSocket is the only one that supports two-way push without re-establishing connections.

Q2. Compare short polling, long polling, SSE, and WebSocket.

Answer:

Feature Short polling Long polling SSE WebSocket
Direction Client pull Client pull (held) Server push Both
Reconnect logic Implicit Implicit Browser auto Manual
Overhead High Medium Low Lowest
Through proxies Yes Yes Yes Sometimes
Browser support All All All except old IE All modern
Best use Rare updates Cheap server push One-way feeds Chat, games, live data

Q3. When would you choose SSE over WebSocket?

Answer:

When the server pushes and the client mostly listens — notifications, live scores, log tailing. SSE runs over plain HTTP so it works through every proxy, has built-in browser auto-reconnect, and is much simpler to implement.

// SSE server (Express)
app.get('/events', (req, res) => {
  res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
  setInterval(() => res.write(`data: ${Date.now()}\n\n`), 1000);
});

// Client
const es = new EventSource('/events');
es.onmessage = (e) => console.log(e.data);

Q4. When NOT to use WebSocket?

Answer:

  • Updates are rare; polling is simpler.
  • Most traffic is one-way; SSE is enough.
  • Behind hostile proxies that block long-lived TCP.
  • The client can't keep a persistent connection (e.g. SEO crawler, batch tool).

WebSockets bring real complexity: scaling, sticky sessions, reconnection, auth on long-lived connections.


2. WebSocket Protocol — Internals

Q5. How does a WebSocket handshake work?

Answer:

A WebSocket starts as an HTTP request with Upgrade: websocket. The client sends a base64 random key; the server responds with a hash of that key plus a magic GUID. After the handshake, the same TCP socket carries WebSocket frames in both directions.

Client request:
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13

Server response:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

After 101, the connection is a WebSocket.

Q6. What does a WebSocket frame look like?

Answer:

A frame has a tiny header (2-14 bytes) and a payload. Key fields:

  • FIN bit: last fragment of a message?
  • Opcode: 0x1 text, 0x2 binary, 0x8 close, 0x9 ping, 0xA pong.
  • Mask bit: client→server frames are always masked.
  • Payload length: 7-bit, 16-bit, or 64-bit.

You normally never deal with frames directly; the library handles them. But knowing this helps when debugging proxy issues.

Q7. What are control frames?

Answer:

Control frames carry connection metadata, not data. The three are close, ping, and pong. They're small (under 125 bytes) and never fragmented. You should respond to a ping with a pong immediately.

Q8. What is a ping/pong and why use it?

Answer:

A ping/pong is a heartbeat. The server sends a ping; if the client doesn't reply with a pong within a timeout, the server considers the connection dead. This catches "half-open" connections where TCP doesn't realize one side is gone (e.g. after a wifi drop).

const ws = new WebSocket('ws://localhost:8080');
ws.on('pong', () => { ws.isAlive = true; });
setInterval(() => {
  if (!ws.isAlive) return ws.terminate();
  ws.isAlive = false;
  ws.ping();
}, 30000);

Q9. Why are client→server frames masked?

Answer:

Masking prevents cache poisoning attacks against legacy HTTP proxies. Without masking, a malicious script could craft bytes that look like an HTTP request and trick a proxy. Masking randomizes the payload so it can't be confused with HTTP. Server→client frames are not masked.


3. Connection Lifecycle

Q10. Walk through the lifecycle of a WebSocket.

Answer:

1. Client opens TCP, sends HTTP Upgrade
2. Server replies 101 Switching Protocols
3. Both sides exchange frames
4. Either side sends a Close frame (opcode 0x8)
5. Other side replies with Close
6. TCP socket closes
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });

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

  ws.on('message', (data) => {
    ws.send(`echo: ${data}`);
  });

  ws.on('close', (code, reason) => {
    console.log('closed', code, reason.toString());
  });

  ws.on('error', (err) => console.error(err));
});
// Client
const ws = new WebSocket('ws://localhost:8080');
ws.onopen = () => ws.send('hello');
ws.onmessage = (e) => console.log(e.data);
ws.onclose = (e) => console.log('closed', e.code, e.reason);

Q11. What close codes should I know?

Answer:

Code Meaning
1000 Normal closure
1001 Going away (page closed)
1006 Abnormal closure (no close frame)
1008 Policy violation
1011 Server error
4000–4999 Application-defined

Use 4xxx codes for app-level reasons like "auth expired" or "rate limited" so your client can react specifically.


4. Reconnection and Reliability

Q12. How should a client reconnect?

Answer:

Use exponential backoff with jitter. Don't reconnect instantly; thousands of clients flooding back at once will crash the server.

let attempt = 0;

function connect() {
  const ws = new WebSocket('wss://example.com/ws');

  ws.onopen = () => { attempt = 0; };

  ws.onclose = () => {
    const base = Math.min(1000 * 2 ** attempt, 30000);
    const delay = base + Math.random() * 1000; // jitter
    attempt++;
    setTimeout(connect, delay);
  };

  ws.onerror = () => ws.close();
}

connect();

Q13. How do you avoid losing messages during reconnect?

Answer:

Two strategies:

  1. Resume by sequence number: server assigns sequence numbers; on reconnect, the client sends "last seen N" and the server replays N+1 onward.
  2. Out-of-band fetch: on reconnect, fetch missed data via REST and then resume the live stream.
ws.onopen = () => ws.send(JSON.stringify({ type: 'resume', lastSeq }));
ws.onmessage = (e) => {
  const msg = JSON.parse(e.data);
  lastSeq = msg.seq;
  handle(msg);
};

Server keeps a small ring buffer per session for replay.

Q14. How do you detect a half-open connection?

Answer:

Half-open means TCP thinks the socket is alive but the other side is gone (laptop closed lid, wifi died). The OS won't tell you for minutes. The fix is application-level heartbeats: server pings every N seconds and closes the socket if no pong arrives.

function heartbeat() { this.isAlive = true; }

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

setInterval(() => {
  wss.clients.forEach((ws) => {
    if (!ws.isAlive) return ws.terminate();
    ws.isAlive = false;
    ws.ping();
  });
}, 30000);

5. Socket.io vs Raw WebSockets

Q15. What does Socket.io give you over raw WebSocket?

Answer:

Socket.io is built on top of WebSockets but adds:

  • Auto-reconnect with backoff.
  • Fallback to long polling when WebSocket isn't available.
  • Rooms and namespaces.
  • Acknowledgements (request-reply per message).
  • Built-in JSON encoding.
  • Adapter ecosystem (Redis, MongoDB) for multi-server fan-out.

Trade-off: Socket.io clients can only talk to Socket.io servers; it's not standard WebSocket on the wire.

Q16. What are namespaces and rooms in Socket.io?

Answer:

  • A namespace is a logical channel like /chat or /admin; each has its own connection event. Use namespaces to split unrelated features.
  • A room is a subset of clients within a namespace; use rooms for per-conversation broadcast.
const io = require('socket.io')(server);
const chat = io.of('/chat');

chat.on('connection', (socket) => {
  socket.on('join', (roomId) => socket.join(roomId));
  socket.on('msg', (roomId, text) => chat.to(roomId).emit('msg', text));
});

Q17. How do acknowledgments work in Socket.io?

Answer:

Either side can pass a callback function as the last argument to emit. The other side calls it to ack. This gives you request-reply over WebSocket.

// Server
socket.on('save', (data, cb) => {
  saveToDb(data);
  cb({ ok: true, id: 42 });
});

// Client
socket.emit('save', { name: 'foo' }, (resp) => {
  console.log('saved', resp.id);
});

6. Authentication and Authorization

Q18. How do you authenticate a WebSocket connection?

Answer:

The handshake is HTTP, so use HTTP auth: cookies, Authorization header, or a query string token.

// Token in query string (simple, OK for short-lived tokens)
const ws = new WebSocket(`wss://api.example.com/ws?token=${jwt}`);

// Server
wss.on('connection', (ws, req) => {
  const url = new URL(req.url, 'http://x');
  const token = url.searchParams.get('token');
  const user = verifyJwt(token);
  if (!user) return ws.close(4401, 'unauthorized');
  ws.user = user;
});

Cookies are nicer because they're set automatically by the browser, but watch out for CSRF if your handshake is on a different origin.

Q19. How do you authorize per-message after auth?

Answer:

Once authenticated, attach the user to the socket and check permissions on every incoming message. Don't trust the client to tell you "I'm user 42" on each message.

ws.on('message', (data) => {
  const msg = JSON.parse(data);
  if (msg.type === 'send' && !canSendTo(ws.user, msg.toUserId)) {
    return ws.close(4403, 'forbidden');
  }
});

Q20. How do you handle token expiration on a long-lived connection?

Answer:

Tokens expire, but a WebSocket can stay open for hours. Two approaches:

  1. Re-auth message: client sends a new token periodically over the socket; server verifies and stores it.
  2. Force reconnect: server closes the socket near token expiry; client reconnects with a fresh token.
// Client refreshes token every 14 minutes for a 15-minute JWT
setInterval(async () => {
  const fresh = await refreshToken();
  ws.send(JSON.stringify({ type: 'auth', token: fresh }));
}, 14 * 60 * 1000);

7. Scaling WebSockets

Q21. Why is scaling WebSockets harder than scaling HTTP?

Answer:

HTTP is stateless and any server can handle any request. WebSockets are long-lived and stateful — a connection lives on one server. To broadcast a message to user X, you have to know which server X is on. As you add servers, the fan-out problem grows.

Q22. How do you broadcast to all users when they're spread across N servers?

Answer:

Use a pub/sub backplane. Each server subscribes to a channel; when one server wants to broadcast, it publishes to the channel and every server delivers to its connected users.

[server 1] - [server 2] - [server 3]
     \            |            /
      \           |           /
        Redis Pub/Sub channel
// Each WS server
const sub = redis.duplicate();
const pub = redis;
await sub.subscribe('broadcast', (msg) => {
  wss.clients.forEach((c) => c.send(msg));
});

// To broadcast from any server:
pub.publish('broadcast', JSON.stringify({ type: 'announce' }));

For Socket.io, the socket.io-redis-adapter does this automatically.

Q23. When should you use Kafka instead of Redis pub/sub for fan-out?

Answer:

Need Choice
Low latency, fire-and-forget Redis pub/sub
Replay missed events Kafka
Strict durability Kafka
Millions of msgs/sec Kafka
Simple, in-memory only Redis

Redis pub/sub drops messages if a subscriber disconnects briefly. Kafka retains and lets the subscriber resume.

Q24. How many WebSocket connections per server?

Answer:

It depends on memory per connection and message rate. A bare Node.js ws server can hold 50,000–100,000 idle connections per process on a normal VM. With heavy message rates, expect 5,000–20,000.

Limits to watch:

  • File descriptors (ulimit -n).
  • Ephemeral port range on the OS.
  • Per-connection memory and timers.

Q25. Sticky vs non-sticky sessions for WebSockets?

Answer:

Sticky. A WebSocket lives on one server; the load balancer must route follow-up frames (and reconnects from the same client) to the same server. ALBs and most proxies support sticky sessions via cookies or source IP. With a pub/sub backplane you can still survive losing a server because reconnect lands on a different one.

Q26. How do you drain WebSocket connections during deployment?

Answer:

When a server is shutting down:

  1. Stop accepting new connections.
  2. Send each client a "please reconnect" message (with a 4xxx close code).
  3. Wait a few seconds with backoff so reconnects spread across remaining servers.
  4. Close any stragglers and exit.
process.on('SIGTERM', async () => {
  server.close(); // stop accepting new connections
  wss.clients.forEach((ws) => ws.close(4001, 'server-shutdown'));
  await new Promise((r) => setTimeout(r, 5000));
  process.exit(0);
});

8. Deployment — Load Balancers and Proxies

Q27. How do you configure Nginx for WebSockets?

Answer:

Nginx needs explicit Upgrade headers and longer timeouts because WebSockets are long-lived.

location /ws {
  proxy_pass http://app:8080;
  proxy_http_version 1.1;
  proxy_set_header Upgrade $http_upgrade;
  proxy_set_header Connection "upgrade";
  proxy_set_header Host $host;
  proxy_read_timeout 3600s;
  proxy_send_timeout 3600s;
}

Without Upgrade and Connection: upgrade, Nginx will treat the request as plain HTTP and the handshake will fail.

Q28. AWS ALB vs NLB for WebSockets?

Answer:

  • ALB: layer 7. Speaks HTTP and WebSocket. Supports cookie-based sticky sessions, path-based routing, and TLS termination. Default choice for most teams.
  • NLB: layer 4. Pure TCP. Lower latency, higher throughput, no TLS termination unless you use TLS listeners. Use when you need extreme performance or non-HTTP protocols.

For 99% of WebSocket workloads, ALB is fine.

Q29. What is the typical WebSocket deployment behind a CDN?

Answer:

CDNs traditionally don't accept WebSockets, but Cloudflare and similar now do. The flow is:

Client -> CDN (TLS termination, DDoS) -> ALB -> WS server -> Redis

Watch out: not every CDN PoP supports WebSocket; check your provider. Idle timeouts are usually 100s, so make heartbeats shorter than that.

Q30. CORS and WebSockets.

Answer:

The WebSocket protocol is not subject to the browser's same-origin policy in the same way HTTP is. The browser sends an Origin header during handshake; the server should validate it.

wss.on('connection', (ws, req) => {
  const origin = req.headers.origin;
  if (!ALLOWED.includes(origin)) return ws.close(4403, 'bad origin');
});

Without this check, any site could open a WebSocket to your server using the visiting user's cookies (cross-site WebSocket hijacking).


9. Kubernetes Considerations

Q31. How do you run WebSockets on Kubernetes?

Answer:

  • Use an ingress controller that supports WebSocket: NGINX Ingress, Traefik, Istio.
  • Set long timeouts (nginx.ingress.kubernetes.io/proxy-read-timeout: "3600").
  • Enable session affinity if your fan-out depends on it; otherwise, use a Redis backplane and let any pod handle any client.
  • Use terminationGracePeriodSeconds long enough for connection draining.
metadata:
  annotations:
    nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
    nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
spec:
  terminationGracePeriodSeconds: 60

Q32. How do you size pods for WebSocket workloads?

Answer:

Memory is usually the limiting factor: each connection needs buffers (~50KB-200KB) plus app state. CPU spikes happen during reconnect storms. Plan for capacity based on:

  • Peak concurrent connections.
  • Heartbeat overhead.
  • Message rate per connection.

Run more, smaller pods to spread blast radius and reconnect load.


10. Common Patterns

Q33. Chat application architecture.

Answer:

[client] --WS--> [edge WS server] --pub--> [Redis pub/sub] --sub--> [other WS servers]
                          |
                          v
                      [Postgres / Cassandra messages table]
  • WebSocket servers terminate connections.
  • Each message is persisted before fan-out.
  • Redis (or Kafka) broadcasts to all WS servers so every recipient gets the message regardless of which server they're on.
  • On reconnect, the client fetches missed messages via REST.
socket.on('message', async ({ roomId, text }) => {
  const msg = await db.insertMessage(roomId, text, socket.user.id);
  await redis.publish(`room:${roomId}`, JSON.stringify(msg));
});

Q34. Real-time dashboard.

Answer:

Server pushes metrics every N seconds. Clients subscribe to specific dashboards.

// Server-side periodic push
setInterval(() => {
  for (const ws of wss.clients) {
    if (ws.dashboard) ws.send(JSON.stringify(getStats(ws.dashboard)));
  }
}, 1000);

Use SSE if updates are server-to-client only; WebSocket if the client also sends commands (zoom, filter, drill-down).

Q35. Notification system.

Answer:

A user has a long-lived WS to receive notifications. The notification service publishes to a Redis channel keyed by user id; the WS server forwards to that user's socket.

sub.subscribe(`user:${ws.user.id}`, (msg) => ws.send(msg));

For users who are offline, store the notification in the database and push a badge count on next connect.

Q36. Collaborative editing (Google Docs style).

Answer:

Two algorithms power this:

  • Operational Transformation (OT): each edit is transformed against concurrent edits.
  • CRDTs (Conflict-free Replicated Data Types): edits commute by construction; no central server needed.

WebSocket carries operations between clients. The server applies them in order and broadcasts to other clients in the same document.

socket.on('op', (op) => {
  const transformed = doc.apply(op);
  socket.to(doc.id).emit('op', transformed);
});

CRDTs (e.g. Yjs, Automerge) are easier today and work peer-to-peer.

Q37. Live multiplayer game.

Answer:

A game server simulates the world tick by tick (e.g. 30 Hz) and sends snapshots/deltas to clients. Clients send input. Latency matters more than throughput. Often UDP via WebRTC is preferred over WebSocket for sub-50ms gameplay.

For turn-based games or simpler real-time games, WebSocket is fine.

Q38. What is backpressure and how do you handle it?

Answer:

Backpressure is when a producer sends faster than a consumer can read. On a WebSocket, the server may try to send 1000 msg/s while the client's network can only handle 100. The OS buffers fill, then the WebSocket library buffers fill, then memory is exhausted.

Detect with ws.bufferedAmount:

if (ws.bufferedAmount > 1_000_000) {
  // 1 MB queued — drop this update or close
  ws.close(4008, 'slow consumer');
  return;
}
ws.send(payload);

Strategies: drop old updates, coalesce updates, slow the source, or kick the slow client.

Q39. Rate limiting on WebSockets.

Answer:

Apply token-bucket per connection. Don't trust the client.

ws.tokens = 10;
setInterval(() => { ws.tokens = Math.min(10, ws.tokens + 1); }, 100);

ws.on('message', (data) => {
  if (ws.tokens-- <= 0) return ws.close(4009, 'rate-limited');
  handle(data);
});

11. Security

Q40. Common WebSocket security risks?

Answer:

  • Cross-site WebSocket hijacking: a malicious site opens a WS to your server using the user's cookies. Validate Origin and use CSRF-style tokens.
  • Lack of origin checks: see above.
  • Insufficient auth on messages: don't trust client claims.
  • DoS via slow clients or many connections: rate-limit, cap connections per IP.
  • Plaintext (ws://): always use wss://.

Q41. Should you use compression on WebSockets?

Answer:

WebSocket per-message-deflate compresses each frame. It saves bandwidth for text but uses CPU. Risks:

  • Slightly amplifies CPU attacks.
  • Some libraries have known bugs (memory leaks).

Enable for chat / large JSON; disable for binary or small frequent messages. Always set a serverMaxWindowBits and limit memory.


12. Monitoring and Operations

Q42. What WebSocket metrics matter?

Answer:

Metric Why
Concurrent connections Capacity planning
Connect / disconnect rate Reconnect storms
Messages/sec in & out Throughput
Avg & p99 message latency User experience
Buffered bytes (backpressure) Slow consumers
Heartbeat failures Half-open detection
Memory per connection Tune capacity

Q43. How do you debug a WebSocket issue?

Answer:

  1. Check the handshake: open browser dev tools → Network → WS tab.
  2. Look at close codes; 1006 means "abnormal" (network/proxy issue).
  3. Verify Nginx / ALB has Upgrade headers and long timeouts.
  4. Confirm heartbeats are flowing.
  5. Use wscat or websocat to test from command line.
wscat -c wss://example.com/ws -H "Authorization: Bearer $TOKEN"

13. WebRTC and When to Use What

Q44. WebRTC vs WebSocket — what's different?

Answer:

WebSocket WebRTC
Topology Client-server Peer-to-peer
Transport TCP UDP-based (with TCP fallback)
Use cases Chat, dashboards Video, voice, low-latency games
Latency Tens of ms Low ms, with packet loss tolerance
Setup Trivial Complex (signaling, ICE, STUN/TURN)

WebRTC also needs a signaling channel — and that signaling channel is usually WebSocket.

Q45. When should you choose what?

Answer:

Use case Choice
Browser-to-server real-time WebSocket
Server pushes only SSE
Rare updates Polling
Voice / video / P2P WebRTC
Real-time game state WebSocket or WebRTC depending on latency

14. Common Pitfalls

Q46. Forgetting heartbeat → silent disconnects.

Answer:

Without app-level heartbeats, a client that lost its network can stay "connected" on the server for minutes or hours. Memory accumulates and messages are sent into the void. Always implement ping/pong with a timeout, both client and server.

Q47. Reconnect storm on deployment.

Answer:

If you restart a server, all clients reconnect at once. With 50k clients and naive instant reconnect, the surviving servers may collapse. Mitigate with:

  • Random jitter on reconnect backoff.
  • Drain connections gradually (Q26).
  • Rolling restart instead of all-at-once.

Q48. State pinned to one instance.

Answer:

Storing per-room state only in the WS server's memory means a server restart loses it and broadcasts can't reach users on other servers. Use Redis or another shared store for state, and pub/sub for fan-out.

Q49. Using polling fallback for new browsers.

Answer:

Socket.io's polling fallback is for legacy browsers. On modern browsers, force WebSocket-only to avoid the polling overhead and quirks.

const io = require('socket.io')(server, { transports: ['websocket'] });

Q50. Treating WebSocket as a pub/sub bus.

Answer:

WebSocket is just a transport. If you build a complex app where many services need to fan out events to many users, don't bolt the pub/sub on the WebSocket protocol; put it in Redis or Kafka behind your WebSocket layer.

Q51. Trusting the client.

Answer:

Treat each incoming message as untrusted input even after auth. Validate types, sizes, and permissions. A logged-in user can still be malicious.

Q52. Final senior-level rules of thumb.

Answer:

  • Default to WebSocket only when you actually need bi-directional push.
  • Always heartbeat and auto-reconnect with jitter.
  • Keep state in a shared store; never pin to one instance.
  • Validate Origin, use wss://, rate-limit per connection.
  • Plan for reconnect storms and slow consumers from day one.
  • Monitor connection counts and buffered bytes, not just CPU.