MERN Stack · Interview Prep

Socket.io & WebSocketsinterview questions & answers

174+ real Socket.io & WebSocketsinterview questions with model answers, plus free lessons to learn the concepts. Prepare in English & Hinglish, then practise with an AI mock interview.

18 topics · 174+ questions

Lessons available in both languages

What you’ll learn

  • What is real-time?
  • HTTP vs WebSocket
  • WebSocket API basicsFree account
  • What is Socket.io?Free account
  • Server setupFree account
  • Client setupFree account
  • Events: emit & onFree account
  • AcknowledgementsFree account
  • BroadcastingFree account
  • RoomsFree account
  • NamespacesFree account
  • Connection lifecycleFree account
  • Express integrationFree account
  • Auth & securityFree account
  • Scaling & recapFree account
  • Project 1: Real-time chat app
  • Project 2: Live notificationsFree account
  • Project 3: Typing indicator & presenceFree account

What is real-time?

Normal HTTP is like sending a letter: you write a request, post it, and wait for a reply. Nothing moves until you ask. If you want to know the latest score, you have to write ANOTHER letter and check again. Real-time is like an open phone call: either side can speak the moment something happens — no one has to "ask" first.

In request-response (HTTP), the server is passive — it can only reply to a request the client sent. It has no way to push data on its own. That's fine for a page load or a form submit, but it breaks down for anything that changes without the user asking: a chat message someone else sent, a live cricket score, a notification ('someone liked your post'), presence ('friend just came online'), or collaborative editing (seeing a teammate's cursor move in a shared doc, like Google Docs). All of these need the SERVER to push the moment it happens — not wait for the next request.

Before WebSocket existed, devs faked real-time with two workarounds:

  • Polling: the client asks "anything new?" every few seconds (setInterval + fetch). Simple, but wasteful (most answers are "no") and laggy (up to that interval's delay).
  • Long-polling: the client asks once, but the server HOLDS the request open and only responds when there's new data (or a timeout) — then the client immediately re-asks. Less wasteful than polling, but still built on request-response underneath, with connection overhead per cycle.
  • WebSocket: a single connection stays OPEN, and either side sends data the instant it wants to — no asking, no reconnecting. This is the real fix, and what Socket.io is built on (we'll wire it up in the next topics).

🌍 Real-world example: WhatsApp Web shows a new message the SECOND your friend sends it — no refresh, no polling delay you can notice. That's only possible because the server pushes over an open connection, not because your browser keeps re-asking.

💡 push = the server sends data to the client on its own, without the client asking first.

💡 polling = client repeatedly asks "anything new?" on a timer — wasteful and laggy.

💡 long-polling = client asks once, server holds the request open until there's data — better than polling but still request-based.

Here's a tiny teaser of what "real" real-time code looks like — a client opening a persistent connection and listening for server pushes (full setup comes in later topics):

Standard definition: Real-time means the server can push data to the client the instant something happens, instead of the client having to repeatedly ask (HTTP request-response) — needed for chat, live scores, notifications, presence, and collaborative editing; achieved today with WebSocket (and libraries like Socket.io built on top of it), after earlier workarounds like polling and long-polling.

// client — a first look at a persistent connection (Socket.io, covered in depth soon)
import { io } from 'socket.io-client';

const socket = io('http://localhost:3000');

socket.on('connect', () => {
  console.log('connected:', socket.id);
});

socket.on('chat message', (msg) => {
  console.log('server pushed:', msg); // arrives instantly, no request needed
});

HTTP vs WebSocket

Think of HTTP as sending letters. You write a letter (request), post it, and wait for a reply (response). The postman never rings your bell to hand you a letter you didn't ask for — the client must always ASK first. Now think of WebSocket as an open phone call. Once the call connects, either person can speak anytime — no one has to "ask permission" to talk. That's the core difference: HTTP is request-response, WebSocket is persistent and full-duplex.

In HTTP, every request opens a connection, gets one response, and (typically) closes. The server can NEVER push data on its own — if the client doesn't ask, it doesn't get an update. That's fine for a page load, but useless for a live chat or a live score that changes without you refreshing.

In WebSocket, there's a one-time handshake: the client sends a normal HTTP request with an Upgrade: websocket header, and if the server agrees, it replies 101 Switching Protocols. After that single handshake, the SAME underlying TCP connection stays open — it's no longer HTTP, both sides can now send messages to each other anytime, in either direction, with low overhead (no repeated headers on every message, unlike HTTP where each request re-sends cookies/headers).

🌍 Real-world example: A live chat app. With plain HTTP you'd have to keep asking "any new messages?" every few seconds (wasteful, laggy). With WebSocket, the server just pushes the new message the instant it arrives — no asking needed.

💡 full-duplex = both sides can send AND receive at the same time, like a phone call (vs HTTP which is like walkie-talkie: one side talks, waits, other replies).

💡 handshake = the one-time HTTP Upgrade request + 101 Switching Protocols response that converts a normal HTTP connection into a WebSocket connection.

💡 ws:// vs wss:// = the WebSocket URL scheme; ws:// is plain (like http://), wss:// is encrypted over TLS (like https://) — always use wss:// in production.

⚠ Gotcha: the handshake is NOT a separate protocol from scratch — it rides on a normal HTTP request. If a proxy/firewall blocks the Upgrade header or doesn't support 101 Switching Protocols, the WebSocket connection fails silently or falls back — this is exactly the kind of infra issue Socket.io's fallback mechanism exists to work around.

When to use which: WebSocket is NOT a replacement for REST/CRUD. For normal request-response work — fetching a user profile, creating an order, submitting a form — plain HTTP is simpler, cacheable, and stateless, so stick with it. Reach for WebSocket only when you need continuous, bidirectional push: chat, live scores, notifications, collaborative editing, presence.

Standard definition: WebSocket is a protocol that upgrades a single HTTP connection (via an Upgrade request answered with 101 Switching Protocols) into a persistent, full-duplex connection where both client and server can send messages at any time with low overhead — unlike HTTP's request-response model where only the client can initiate.

// ---------- Client: opening a raw WebSocket connection ----------
const ws = new WebSocket('wss://example.com/socket'); // wss:// = encrypted (TLS)

ws.onopen = () => {
  console.log('Handshake done — connection is now persistent & full-duplex');
  ws.send('hello server');            // client CAN push anytime, no asking
};

ws.onmessage = (event) => {
  console.log('Server pushed:', event.data); // server CAN push anytime too
};

// ---------- Compare: HTTP — client must always ask ----------
// fetch('https://example.com/messages')   // one request -> one response, then closed
//   .then(res => res.json())
//   .then(data => console.log(data));      // server can NEVER send this on its own

Project 1: Real-time chat app

What we're building: A real-time chat app end-to-end — an Express server that also runs Socket.io on the same port, a browser client that connects, and messages that broadcast INSTANTLY to everyone (no refresh, no polling). This is THE resume project for real-time — it's the one interviewers ask you to walk through: "how does a message get from your browser to everyone else's screen?"

By the end you'll trace one message's entire journey: user types → client socket.emit → server socket.on receives it → server io.emit (or io.to(room).emit) broadcasts it → every connected client's socket.on fires → the UI renders the new message. Master this loop and you understand real-time architecture, not just Socket.io syntax.

🌍 Real-world example: This is exactly WhatsApp Web / Slack / Discord's core loop. You send a message, it appears on your own screen AND on every other open tab/device in that conversation — instantly, without anyone refreshing. Swap our in-memory rooms for a database-backed room list and this is production.

💡 Broadcast = one message, sent by the server to many clients at once. That's the whole point of Socket.io over plain HTTP — the server can PUSH without being asked.


Step 0 — The mental model (read this first)

A normal Express route runs once per HTTP request and dies. A chat app needs the opposite: a connection that STAYS OPEN so the server can push a message to a client at any moment, even if that client didn't ask for anything. That's what Socket.io gives us — one persistent connection per browser tab, living on top of the SAME http server Express uses.

The skeleton has exactly three moving parts:

  1. One shared server — Express handles normal routes, Socket.io handles the real-time channel, both on the same port.
  2. Server-side event wiringio.on('connection', socket => ...) fires once per connecting client; inside it we listen for socket.on('chat message', ...).
  3. Client-side event wiring — connect, socket.emit to send, socket.on to receive and render.

Every feature we add (username, rooms) is just MORE data riding on the same emit/on wiring — the skeleton never changes.


Step 1 — One shared http server (Express + Socket.io on one port)

Socket.io needs a raw Node http.Server to attach to — not the Express app object directly. So we create the http server ourselves and hand it to BOTH:

const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
app.use(express.static('public')); // serves index.html + client.js

const server = http.createServer(app);          // ONE server
const io = new Server(server, {                  // Socket.io attaches to it
  cors: { origin: '*' }
});

server.listen(3000, () => console.log('Chat server on http://localhost:3000'));

What's happening:

  1. http.createServer(app) wraps our Express app in a raw Node http server — Express alone can't be handed to Socket.io, but this wrapped server object can.
  2. new Server(server, {...}) attaches Socket.io to that SAME server. Now one port (3000) answers BOTH normal HTTP requests (Express routes, static files) AND the WebSocket upgrade handshake (Socket.io).
  3. We server.listen(...), NOT app.listen(...)app.listen would create its own separate http server that Socket.io never sees.
  4. cors: { origin: '*' } allows the browser client (which may be served from a different origin during dev) to connect.

💡 This is the #1 gotcha interviewers check: if you app.listen() and separately try to attach io to app, it silently fails to share the port. Socket.io MUST attach to the http.Server, and Express MUST be the request handler passed into http.createServer().


Step 2 — Server: io.on('connection') + socket.on('chat message') → io.emit

This is the heart of the whole app — the server-side wiring that turns one incoming message into a broadcast to everyone:

io.on('connection', (socket) => {
  console.log('a user connected:', socket.id);

  socket.on('chat message', (msg) => {
    console.log('message:', msg);
    io.emit('chat message', msg);   // send to EVERY connected client, incl. sender
  });

  socket.on('disconnect', () => {
    console.log('user disconnected:', socket.id);
  });
});

What's happening:

  1. io.on('connection', socket => ...) — Socket.io calls this callback ONCE per browser tab that connects. socket is that ONE client's private channel; io (used below) means ALL clients.
  2. Inside, socket.on('chat message', msg => ...) listens for a custom event named 'chat message' — this name is arbitrary, WE chose it, and the client must emit the exact same string.
  3. When a message arrives, io.emit('chat message', msg) re-broadcasts it to EVERY connected socket (including the one that sent it) — that's why the sender sees their own message appear too, same as everyone else.
  4. socket.on('disconnect', ...) fires when that tab closes or loses connection — good place to clean up (we'll use it for presence later).

💡 socket.on vs io.on: io.on('connection', ...) is the ONE-TIME setup per client. socket.on('chat message', ...) is a PER-EVENT listener on that one client's channel. io.emit fans out to everyone; socket.emit would only reply to that one sender.


Step 3 — Client: connect, send on submit, render incoming

On the browser side (plain JS or inside a React useEffect — the wiring is identical):

<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io();   // connects to the server that served this page

  const form = document.getElementById('form');
  const input = document.getElementById('input');
  const messages = document.getElementById('messages');

  form.addEventListener('submit', (e) => {
    e.preventDefault();
    if (input.value) {
      socket.emit('chat message', input.value);   // SEND
      input.value = '';
    }
  });

  socket.on('chat message', (msg) => {              // RECEIVE
    const item = document.createElement('li');
    item.textContent = msg;
    messages.appendChild(item);
  });
</script>

What's happening:

  1. io() with no arguments connects back to whatever host served the page — Socket.io's client script (auto-served at /socket.io/socket.io.js by the server) makes this available.
  2. On form submit we preventDefault() (no page reload) and socket.emit('chat message', input.value) — this is the exact string the server's socket.on('chat message', ...) is listening for.
  3. socket.on('chat message', msg => ...) is the client's OWN listener — it fires for every broadcast the server sends, including messages FROM this same client (because server used io.emit, not socket.broadcast.emit).
  4. Rendering is just DOM manipulation: append a new <li> for each incoming message. In React this would be setMessages(prev => [...prev, msg]) inside the same socket.on callback, registered in a useEffect with cleanup socket.off('chat message', handler) on unmount.

💡 Same event name, both directions. The client emits 'chat message' and ALSO listens for 'chat message' — that's normal; the server is the one relaying it back out to everyone, sender included.


⭐ Step 3.5 — TRACE one message end-to-end

Follow ONE message through the entire system, beat by beat. Say user Aisha types "hi everyone" and hits Enter, with Bilal's browser also connected:

  1. Aisha's browser: form submit fires → socket.emit('chat message', 'hi everyone') sends the string down Aisha's OWN socket connection to the server.
  2. Server receives it: the socket.on('chat message', msg => ...) callback registered on Aisha's socket (inside her io.on('connection', ...) block) fires with msg = 'hi everyone'.
  3. Server rebroadcasts: inside that handler, io.emit('chat message', msg) runs — this reaches EVERY connected client's socket, not just Aisha's.
  4. Aisha's browser receives it back: her own socket.on('chat message', ...) listener fires with 'hi everyone' — she sees her own message appear in the list (this is io.emit's behavior — includes the sender).
  5. Bilal's browser receives it: his socket.on('chat message', ...) listener ALSO fires with the same 'hi everyone', because he's a connected socket and io.emit reaches all of them.
  6. Both UIs update: each browser's listener callback runs its own document.createElement('li') (or setMessages) — so Aisha and Bilal both see "hi everyone" appear in their message list, at effectively the same instant, with NO page reload and NO one polling for it.

The one-way HTTP request that started it all (the initial page load) is long finished. Everything after that — Aisha's message reaching Bilal — travels over the STILL-OPEN Socket.io connection. That persistent connection is the entire reason this feels instant.


Step 4 — Add a username

Right now every message is anonymous. We attach a username to the payload, either from a login prompt or a simple form field:

// client: send { user, text } instead of a bare string
socket.emit('chat message', { user: username, text: input.value });

// client: render both
socket.on('chat message', (msg) => {
  const item = document.createElement('li');
  item.textContent = `${msg.user}: ${msg.text}`;
  messages.appendChild(item);
});

// server: unchanged wiring, just relays whatever shape the payload is
socket.on('chat message', (msg) => {
  io.emit('chat message', msg);   // { user, text } passes straight through
});

What's happening: the event name ('chat message') and the emit→on wiring stay EXACTLY the same — we just changed the payload from a plain string to an object { user, text }. Socket.io serializes objects as JSON automatically, no extra work. The server doesn't even need to know the shape changed — it's just relaying whatever it receives. This is the pattern for adding ANY new field later (timestamp, avatar, message id): grow the payload object, not the wiring.

💡 A more secure version stores username on the server at connection time (socket.data.username = name) so a client can't spoof someone else's name in the payload — worth mentioning in interviews as the production-grade version.


Step 5 — Add ROOMS (join a room, scope broadcasts)

Right now io.emit sends to EVERY connected client — fine for one global chat, wrong for a chat app with multiple rooms. socket.join(room) puts a socket into a named room; io.to(room).emit(...) sends ONLY to sockets in that room:

// client: join a room after connecting
socket.emit('join room', roomName);

// client: send within that room context
socket.emit('chat message', { room: roomName, user: username, text: input.value });

// server: join + scoped broadcast
io.on('connection', (socket) => {
  socket.on('join room', (room) => {
    socket.join(room);
    console.log(`${socket.id} joined room ${room}`);
  });

  socket.on('chat message', (msg) => {
    io.to(msg.room).emit('chat message', msg);   // ONLY sockets in msg.room receive it
  });

  socket.on('disconnect', () => {
    console.log('user disconnected:', socket.id);
  });
});

What's happening:

  1. socket.join(room) is a SERVER-side call — the client just emits a 'join room' event with the room name; the server is the one that actually adds that socket to the room. Rooms are invisible to the client — it's bookkeeping Socket.io keeps internally per socket.
  2. io.to(room).emit(event, data) is io.emit's scoped cousin — instead of reaching every socket globally, it reaches only sockets that called socket.join(room) for that exact room name.
  3. We now tag each message with msg.room so the server knows WHICH room to broadcast into — the client includes it in the payload it emits.
  4. A socket can be in multiple rooms at once, and leaves with socket.leave(room) (e.g., when switching rooms) — otherwise it silently keeps receiving messages from rooms it already left visually.

💡 io.emit vs io.to(room).emit: io.emit = everyone on the server, no exceptions. io.to(room).emit = only sockets that joined that room. Swapping global chat → per-room chat is JUST replacing io.emit with io.to(msg.room).emit plus the join room step — the emit→on skeleton from Step 2/3 doesn't change at all.


🔎 The full flow (recap the wiring)

Every chat message follows the SAME beat, room-scoped or not:

  1. Client emitssocket.emit('chat message', payload) sends up THIS client's own connection.
  2. Server's socket.on receives it — inside io.on('connection', socket => ...), one listener per connected client.
  3. Server rebroadcastsio.emit(...) (everyone) or io.to(room).emit(...) (just that room).
  4. Every target client's socket.on fires — including the original sender's, because broadcasts don't exclude by default.
  5. Each client renders — the listener callback updates the DOM (or React state) with the new message.

And the wiring never changes shape as you add features — username and room are just MORE FIELDS in the same payload object riding the same 'chat message' emit/on pair.


🚀 Where this goes next

This skeleton — shared server, io.on('connection'), socket.on/io.emit, rooms — is the base for EVERY real-time feature: typing indicators (socket.to(room).emit('typing', ...), excluding the sender), online presence (track sockets on connect/disconnect), read receipts, live notifications. You already know the whole pattern; only the event name and payload shape change.


✅ What you just learned

  • One shared http server for Express + Socket.io — http.createServer(app) then new Server(server, {...}), and server.listen(...) NOT app.listen(...).
  • io.on('connection') fires per client; inside, socket.on('chat message', ...) listens for that client's messages.
  • io.emit broadcasts to everyone (including the sender) — the mechanism that makes a chat feel instant to all open tabs.
  • Client wiringsocket.emit to send on submit, socket.on to receive and render, same event name both directions.
  • Tracing a message end-to-end: client emit → server socket.on → server io.emit/io.to → every target client's socket.on → UI render.
  • Growing the payload, not the wiring — adding username is just { user, text } instead of a bare string.
  • Roomssocket.join(room) (server-side) + io.to(room).emit(...) scopes broadcasts to only that room's sockets.
// server.js
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');

const app = express();
app.use(express.static('public'));

const server = http.createServer(app);
const io = new Server(server, { cors: { origin: '*' } });

io.on('connection', (socket) => {
  console.log('a user connected:', socket.id);

  socket.on('join room', (room) => {
    socket.join(room);
  });

  socket.on('chat message', (msg) => {
    // msg = { room, user, text }
    if (msg.room) {
      io.to(msg.room).emit('chat message', msg);
    } else {
      io.emit('chat message', msg); // global fallback
    }
  });

  socket.on('disconnect', () => {
    console.log('user disconnected:', socket.id);
  });
});

server.listen(3000, () => console.log('Chat server on http://localhost:3000'));

// public/client.js (or a React useEffect)
const socket = io();
const username = prompt('Your name?') || 'Anonymous';
const room = 'general';
socket.emit('join room', room);

form.addEventListener('submit', (e) => {
  e.preventDefault();
  if (input.value) {
    socket.emit('chat message', { room, user: username, text: input.value });
    input.value = '';
  }
});

socket.on('chat message', (msg) => {
  const item = document.createElement('li');
  item.textContent = `${msg.user}: ${msg.text}`;
  messages.appendChild(item);
});

Socket.io & WebSocketsinterview questions & answers

10 sample questions below — 174+ in the full bank inside.

What is socket.id and why is it unique?

socket.id is a unique identifier assigned by Socket.io to each connected client. It's generated automatically when the connection is established and is unique within that server instance. You use it to identify and send messages to a specific client.

In simple terms: Like a ticket number at a shop—each customer gets one when they enter so the shop knows who to call. Example: if socket.id = '12345abc', you can send a message only to that client with io.to('12345abc').emit('message', data).

What is the difference between emit and on in Socket.io?

emit() SENDS a named event from one side to the other; on() LISTENS and receives that event. If the server emits 'message', the client must have a listener socket.on('message') to receive it. They work in pairs — one emits, the other listens.

In simple terms: Think of emit like sending a letter and on like a mailbox that catches it. The postman (emit) puts the letter in the box (on). Example: server emits 'notification' → all clients listening with socket.on('notification', (data) => console.log(data)) will receive it.

What is the difference between socket.emit() and io.emit()? Write the code.

`socket.emit('event', data)` sends only to that ONE client. `io.emit('event', data)` (on the server) broadcasts to ALL connected clients, including the sender. Code: `io.emit('notification', { text: 'Server update' })` reaches everyone.

In simple terms: socket.emit = SMS to one person; io.emit = announcement to everyone in the room. Both come from the server. Example: when a new user joins, `io.emit('user-count', 5)` tells ALL clients the count updated, but `socket.emit('welcome', {name})` says hello to just that new user.

What happens when a client connects to a Socket.io server?

The server fires an 'connection' event, receiving a socket object that represents that unique client. Inside the handler, you can listen for events from that client, emit events back to it, or add it to rooms. Each connected client gets its own socket instance.

In simple terms: Think of it like a waiter greeting a customer when they sit at a table. The table (socket) is now 'reserved' for that customer; the waiter knows which table to serve. Example: io.on('connection', (socket) => { console.log('Client connected:', socket.id); }).

Write the client-side code to emit an event WITH an acknowledgement callback.

socket.emit('save', { name: 'John', age: 25 }, (ackResponse) => { console.log('Server responded:', ackResponse); if (ackResponse.success) { console.log('Data saved successfully'); } }); The callback (last arg) runs when the server calls ack. The `ackResponse` contains the server's reply.

In simple terms: The callback is your 'mailbox' for the reply. When you emit with `socket.emit('event', data, callback)`, you're asking the server to process and respond. Example: user submits a form, you emit with a callback, server validates and calls back with success/error. It's one request-response pair over the WebSocket.

What does socket.emit() do? Who receives the message?

socket.emit() sends a message ONLY to that specific client (the one that triggered it). It does not broadcast to other clients. Use it when you want to send a private response — e.g., sending back a confirmation or error to just that one user.

In simple terms: Think of socket.emit as sending an SMS to ONE person (only the sender's phone gets it). Example: server receives a 'save' event, does work, then `socket.emit('save-confirm', { status: 'ok' })` replies to just that client.

Write the server-side code to receive an event and send an acknowledgement.

socket.on('save', (data, ack) => { console.log('Received:', data); // process the data const result = { success: true, id: 123 }; ack(result); // call ack to send response back to client }); The server listener receives TWO args: `data` (what client sent) and `ack` (the callback function). Call `ack(response)` to send data back to the client.

In simple terms: On the server, `ack` is the callback the client passed. When you call `ack(result)`, it sends `result` directly to the client's callback. It's like: server receives the package, processes it, signs the form (calls ack), and sends the confirmation back — all over the same WebSocket.

Write the Socket.io code for a simple chat where the client sends a message and the server receives it.

Client: socket.emit('chat message', { text: 'Hello' }); Server: socket.on('chat message', (data) => { console.log(data.text); });

In simple terms: The client uses emit to send data with a custom event name 'chat message'. The server listens on the same event name with on and receives the data in a callback. The event name acts like a label so both sides know what type of message this is.

What is an acknowledgement in Socket.io?

An acknowledgement (ack) is a callback function passed as the LAST argument to `socket.emit()`. When the server receives the event, it calls this callback to confirm receipt or send back data — like request-response inside a WebSocket.

In simple terms: Think of ack like a delivery confirmation: you send a letter (emit with a callback) and the recipient signs to confirm they got it (server calls the callback). Example: `socket.emit('save', { data }, (response) => console.log('Saved!'));` The server receives, saves, and calls the callback with the result.

Name three built-in/reserved events in Socket.io that you cannot use as custom event names.

connect (when a client connects), disconnect (when a client disconnects), and connect_error (when a connection error occurs). These are special Socket.io events that fire automatically; you listen for them but cannot use them as custom event names.

In simple terms: Socket.io reserves some event names for its own use. When you create a client connection, connect fires automatically. When the client leaves or the network drops, disconnect fires. If auth fails, connect_error fires. These are housekeeping events, not custom messages — example: socket.on('connect', () => console.log('I am online')).

164+ more Socket.io & WebSockets questions inside

Create a free account to read the full question bank, learn every topic, and practise with an AI mock interview.

Unlock all questions — free

Ready to practise Socket.io & WebSockets?

Unlock every topic free, then face an AI interviewer that asks follow-ups and grades your answers.