We now understand what Socket.IO is and why it is useful. In this post, we go one level deeper and look at what actually happens behind the scenes when a Socket.IO connection is created.
Two Layers of Socket.IO
Socket.IO is actually built from two separate layers, and understanding this split makes everything else much easier to follow:
- Engine.IO — the low-level layer. It handles the actual connection: opening it, choosing a transport, upgrading it, and keeping it alive.
- Socket.IO — the high-level layer built on top of Engine.IO. It adds events, rooms, namespaces, and acknowledgments.
A simple way to remember it: Engine.IO keeps the connection alive. Socket.IO organizes what travels through it.
Why Socket.IO Does Not Connect With WebSocket First
This surprises a lot of beginners. You would expect Socket.IO to try a WebSocket connection immediately, but it does not. By default, it always starts with HTTP long-polling, and only upgrades to WebSocket afterward, if possible.
The reason is reliability. A WebSocket connection can fail silently in certain environments, such as behind strict corporate proxies, antivirus software, or misconfigured firewalls. HTTP long-polling almost always works, since it looks like a normal HTTP request. So Socket.IO plays it safe first, gets you connected quickly using long-polling, and then quietly tries to upgrade the connection to something better in the background.
Step-by-Step: What Happens When You Connect
Step 1: The initial handshake (long-polling) The client sends an HTTP request to the server, asking to open a connection. The server responds with important setup information, including:
- A unique session ID (
sid) for this connection - A list of transports the server can upgrade to (usually
["websocket"]) pingIntervalandpingTimeoutvalues, used later for the heartbeat mechanism
Step 2: Communication begins over long-polling At this point, the client and server can already exchange messages using repeated HTTP requests. It works, but it is not yet the most efficient option available.
Step 3: Testing an upgrade to WebSocket While the long-polling connection is still active, Socket.IO tries to open a WebSocket connection on the side, as a kind of test. It sends a small "probe" packet over this new WebSocket connection to check if it works properly.
Step 4: Switching over If the probe succeeds, the client tells the server it wants to switch. Once confirmed, all further communication moves to the WebSocket connection, and the old long-polling connection is closed. This entire process usually happens within a fraction of a second, so from your point of view as a developer, it feels instant.
Step 5: Falling back, if needed
If the WebSocket probe fails, for example because a network is blocking WebSocket connections, Socket.IO simply continues using long-polling. Your application code does not need to know or care which transport ended up being used. The event-based API (socket.emit, socket.on) works exactly the same either way.
The Heartbeat Mechanism
Once connected, Engine.IO keeps checking that both sides are still alive, using the pingInterval and pingTimeout values shared during the handshake:
- At every
pingInterval, the server sends a small ping packet - The client must reply with a pong packet
- If no pong arrives within
pingTimeout, the server treats the connection as dead - Similarly, if the client does not receive a ping in time, it treats the connection as dead
This is how Socket.IO detects broken connections quickly, instead of waiting indefinitely for something to fail.
A Third Transport: WebTransport
Newer versions of Socket.IO also support a third transport option called WebTransport, which is built on top of HTTP/3. It is especially useful in unstable network conditions, since it handles packet loss better than a traditional WebSocket connection. It is not enabled by default and browser support is still growing, but it shows the direction Socket.IO is heading in terms of performance.
So in modern Socket.IO, there are three possible transports:
- HTTP long-polling (the safe starting point)
- WebSocket (the common, efficient upgrade)
- WebTransport (the newer, opt-in option for supported environments)
Visualizing the Flow
Client Server
|--- HTTP request (open) ------>|
|<-- sid, upgrades, ping info --|
|--- long-polling messages ---->| (connection active)
|<-- long-polling messages -----|
|
|--- probe over WebSocket ----->| (tested quietly, in background)
|<-- probe confirmed -----------|
|
|=== switched to WebSocket ====| (long-polling connection closed)
|<------ ping ------------------|
|------- pong ------------------>|
Why This Design Matters for You as a Developer
You will almost never touch Engine.IO directly in your projects, but understanding this flow helps in real situations:
- If you ever see a Socket.IO connection stuck on long-polling instead of WebSocket, you now know it usually means the WebSocket upgrade probe failed, often due to a network or proxy issue
- If you see repeated
ping/pong-related disconnects, you know exactly which mechanism is responsible - It explains why Socket.IO feels more "reliable" than raw WebSockets in unpredictable network conditions
Summary
- Socket.IO is built on two layers: Engine.IO (connection and transport) and Socket.IO (events, rooms, namespaces)
- By default, it starts with HTTP long-polling, then tries to upgrade to WebSocket in the background
- If the WebSocket upgrade fails, it silently continues using long-polling, and your code does not need to change
- A heartbeat mechanism (ping/pong) constantly checks that the connection is still alive
- Newer versions also support WebTransport as an additional, more efficient transport option
This completes Phase 1. In the next post, we begin Phase 2 by creating a fresh Next.js 16 project with TypeScript and Tailwind CSS, the foundation for the rest of this course.