Lessons available in both languages
MERN Stack · Interview Prep

Socket.io & WebSockets interview questions & answers

174+ real Socket.io & WebSockets interview 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

How Hirenix teaches

One chapter. 90 minutes.
Interview-ready.

Every concept starts with a real-world problem — the kind that actually shows up in production code. Nothing to cram; it just clicks. Every question comes with a model answer: exactly what to say in the room, and why. Then an AI mock interview on the same chapter.

  • 📖Concept in 5 minutesNo jargon — straight to the point
  • 🛠️Real-world problemThe kind production code throws at you
  • 💬Model answerExactly what to say in the room
  • 🧠FlashcardsRevise in 10 minutes
  • 🤖AI mock interviewIt asks follow-ups too
  • 📊Weak topicsSee exactly where you're stuck
Start this chapter — free🌐 English🇮🇳 Hinglish
A student learning an interview concept on Hirenix at home
Video playlistbuilt around a syllabus18h+
Hirenix chapterbuilt around interviews90 min

The difference isn’t the content — it’s the filter. Only what’s actually used in production and actually asked in interviews. Textbook topics the industry never touches don’t make the cut.

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

When you genuinely need real-time: the server has information the client did not ask for and cannot predict — a message arriving, another user typing, a price moving, a job finishing. Anything where waiting for the user to refresh means showing stale data.

When polling is the better engineering choice: updates are infrequent or a small delay is acceptable. Checking every 30 seconds is a few lines, works through every proxy, needs no connection state, and scales on infrastructure you already have. Reaching for WebSockets to update a dashboard once a minute is complexity with no user-visible gain.

Trade-off: real-time removes latency and adds a stateful, long-lived connection to a system that was stateless. You now have reconnection, connection limits, sticky sessions and per-connection memory to think about — costs that only appear once you have real users, which is exactly why the decision deserves thought before the demo.

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.

When HTTP is still the right tool: anything the client initiates and the server answers — loading a page, submitting a form, fetching a list. HTTP is cacheable, stateless, understood by every proxy and CDN, and scales horizontally without thought.

When only a WebSocket will do: the server must speak first. No amount of HTTP cleverness gives you a true server push; long-polling simulates it at the cost of a request per message.

When to use both: almost every real app does. Load the page and its data over HTTP, then open a socket for the live updates. They are not competitors.

Trade-off: a WebSocket trades HTTP's statelessness for a persistent connection — and statelessness was doing a lot of quiet work. Load balancers must keep a client on one server, each connection holds memory for its whole lifetime, and a deploy drops every connection at once. That is affordable and it is not free.

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);
    console.log(`${socket.id} joined room ${room}`);
  });

  socket.on('chat message', (msg) => {
    // msg = { room, user, text }
    console.log('message:', msg);
    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 form = document.getElementById('form');
const input = document.getElementById('input');
const messages = document.getElementById('messages');
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.

Give 3 examples where you actually need real-time features in an app.

Chat applications (messages appear instantly), live notifications (order status, friend requests), live scores or dashboards (stock prices, sports scores), collaborative editing (Google Docs, multiple users editing at once), presence indicators (see who's online), and live gaming. Any feature where instant updates improve the user experience.

In simple terms: Real-time isn't always needed — fetching weather every hour is fine with polling. But when users expect immediate feedback (chat, notifications, live scores), polling is too slow. Think: if you're watching a live cricket match, a 5-second delay ruins it.

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 same socket.

What's the main problem with HTTP when you need real-time updates?

HTTP requires the client to always ask the server for new data. The server cannot push updates to the client automatically. So for real-time features like chat or live notifications, HTTP forces the client to keep asking 'Any new messages?'—which is wasteful and slow.

In simple terms: Imagine a mailbox where you're waiting for a letter. With HTTP, you have to go check the mailbox every 5 seconds; you can't just wait for the mail carrier to ring a bell and hand it to you. So you waste time checking an empty mailbox. Example: a chat app using HTTP has to ask the server 'new messages?' every 2 seconds, even if nothing has changed.

What is polling, and why is it a problem for real-time features?

Polling is when the client repeatedly asks the server 'any updates?' every few seconds. It's a workaround for HTTP's limitation. The problem: if you poll every 5 seconds, updates arrive 5 seconds late (lag), and it wastes bandwidth and battery with constant requests even when nothing changed.

In simple terms: Polling is like calling a pizza restaurant every minute to ask if your order is ready — annoying, slow (you miss updates between calls), and the restaurant gets exhausted answering the same question. A better approach: the restaurant calls you the moment it's ready.

What does the Socket.io Server constructor do?

The Server constructor creates a Socket.io server instance that listens for WebSocket connections. It takes an http.Server object and options (like CORS config) and sets up the real-time communication layer on top of your Node.js HTTP server.

In simple terms: Think of it like building a telephone switchboard — the http.Server is the building's wiring, and the Server constructor adds the switchboard that lets callers (clients) connect and talk. Without it, your HTTP server only handles request-response (like letters); the Server constructor adds the ability for the server to push messages back anytime. For example: const io = new Server(httpServer, { cors: {...} });

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 over the same connection.

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) — as long as the connection stays up long enough for that callback to run back. Example: socket.emit('save', { data }, (response) => console.log('Saved!')); The server receives, saves, and calls the callback with the result.

What is socket.id in Socket.io?

socket.id is a unique identifier automatically assigned to each connected client. It's a string that remains the same during that connection session, making it easy to target a specific client for direct messages or to track individual users.

In simple terms: Think of it like a seat number in a movie theater — each person who walks in gets assigned a unique seat number so you can find them again, pass them popcorn, or tell them about a message. When they leave and come back later, they get a NEW seat number. Example: when a client connects, you might log socket.id to see their unique ID: socket.id => 'abc123xyz...'.

Why do we need to configure CORS in Socket.io?

CORS (Cross-Origin Resource Sharing) controls which domains are allowed to connect to your Socket.io server. Without proper CORS configuration, browsers block connections from frontend apps running on different origins (different domain, port, or protocol), even if they're yours.

In simple terms: Think of CORS as a bouncer at a nightclub — the bouncer checks your ID to see if you're on the VIP list (allowed origins). If your frontend runs on http://localhost:3000 but the server is on http://localhost:5000, the browser blocks the socket connection unless you tell it 'hey, localhost:3000 is allowed to talk to me.' Example: cors: { origin: 'http://localhost:3000' }.

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')).

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 connection.

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.