PHASE 2 — Topic 7: Setting Up a Custom Node.js Server Alongside Next.js for Socket.IO

We understood why Socket.IO needs a persistent server. Now let's actually build it. By the end of this post, your project will run through a custom server instead of the default next dev process.

Step 1: Install the Required Packages

We will use tsx to run our TypeScript server directly, without a separate compile step. From your project root, run:


    npm install socket.io
    npm install --save-dev tsx @types/node cross-env

We are only installing socket.io for now (the server-side package). We will install socket.io-client in the next post, when we build the client side.

Step 2: Create the Custom Server File

Create a new file called server.ts at the root of your project, at the same level as package.json (outside the src folder).


    import { createServer } from "http";
    import { parse } from "url";
    import next from "next";
    import { Server } from "socket.io";

    const dev = process.env.NODE_ENV !== "production";
    const hostname = "localhost";
    const port = parseInt(process.env.PORT || "3000", 10);

    const app = next({ dev, hostname, port });
    const handle = app.getRequestHandler();

    app.prepare().then(() => {
        const httpServer = createServer((req, res) => {
            const parsedUrl = parse(req.url!, true);
            handle(req, res, parsedUrl);
        });

        const io = new Server(httpServer, {
            path: "/api/socket",
        });

        io.on("connection", (socket) => {
            console.log("A client connected:", socket.id);

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

        httpServer.listen(port, () => {
            console.log(`Server ready on http://${hostname}:${port}`);
        });
    });

Let's break down what each part is doing:

  • next({ dev, hostname, port }) creates the Next.js app instance, the same one that normally runs behind next dev
  • handle is the function that lets Next.js process every normal page and route request
  • createServer(...) creates one plain Node.js HTTP server, and every incoming request is handed off to Next.js through handle
  • new Server(httpServer, ...) attaches Socket.IO to that exact same HTTP server, instead of creating a separate one
  • The path: "/api/socket" option tells Socket.IO which URL path to listen on for its own connections, so it does not clash with your normal Next.js routes
  • io.on("connection", ...) runs every time a new client connects, and we currently just log it

Step 3: Update Your package.json Scripts

Open package.json and replace the existing scripts section with this:


  "scripts": {
    "dev": "tsx watch server.ts",
    "build": "next build",
    "start": "cross-env NODE_ENV=production tsx server.ts",
    "lint": "eslint"
  }

Here is what changed:

  • dev now runs server.ts using tsx watch, which restarts the server automatically whenever you save a file, similar to how next dev behaved before
  • start runs the same server file, but with NODE_ENV=production, so Next.js knows to serve the optimized production build instead of development mode
  • build stays exactly the same, since building the Next.js app is unrelated to which server runs it afterward

Step 4: Run the Custom Server

Stop any previous next dev process that might still be running, then start the project again with:


    npm run dev

You should see this in your terminal:


    Server ready on http://localhost:3000

Open http://localhost:3000 in your browser. Your Next.js app should load exactly as before. Nothing looks different on the surface, but under the hood, your app is now running through the custom server we just built, with Socket.IO already attached and listening.

Step 5: Confirm Socket.IO Is Actually Running

Right now, nothing in our frontend tries to connect yet, so you will not see any connection logs. That is expected, since we have not written any client-side code. We are simply confirming the server starts correctly and is ready to accept connections once we do.

A Note on next build

One important detail: running next build will still work fine, since it only builds the Next.js application itself. But you must always start the app afterward using npm run start, which runs through server.ts. Running the plain next start command directly will not include Socket.IO at all, since that command boots the default Next.js server, not our custom one.

Summary

  • We installed socket.io and tsx, then created a server.ts file at the project root
  • This file creates one plain HTTP server, hands normal requests to Next.js, and attaches Socket.IO to the same server on a dedicated path
  • We updated package.json so both dev and start run through this custom server instead of the default Next.js server
  • The app currently starts correctly with Socket.IO attached, but nothing connects to it yet, since we have not written any client-side code

In the next post, we will install socket.io-client, add proper TypeScript types, and connect our React frontend to this server for the first time.

PHASE 2 — Topic 6: Why Socket.IO Needs a Persistent Server, and How This Fits Into Next.js

Before we write a single line of Socket.IO code, we need to understand a very important architectural fact. This is the part beginners often skip, and then get stuck later wondering why their Socket.IO code "just doesn't work" once deployed. Let's clear this up properly.

How Next.js Normally Runs

By default, modern Next.js is built around serverless functions and Automatic Static Optimization. When you deploy a Next.js app to a platform like Vercel, most of your code does not run on one continuously running server. Instead:

  • Each request spins up a function
  • That function handles the request
  • Once it responds, it shuts down again

This is great for performance, cost, and scaling, since you only pay for what you use, and idle time costs nothing.

Why This Breaks Socket.IO

Socket.IO, as we learned earlier, needs a connection that stays open continuously, so the server can push data to the client at any time. But serverless functions are the opposite of that. They are designed to start, respond, and terminate as quickly as possible. They are not meant to hold a connection open and wait.

This creates a direct conflict:

  • Socket.IO needs: one server, staying alive, holding open connections
  • Default Next.js deployment gives you: many short-lived functions, spun up per request

Because of this, a serverless function simply cannot hold a Socket.IO (or any WebSocket) connection open. The function terminates almost immediately after responding, closing any connection along with it.

The Solution: A Custom Server

To make Socket.IO work with Next.js, we need a custom server. This means we do not let Next.js manage its own server internally. Instead, we create our own Node.js server, and tell Next.js to run inside it.

Here is the important part: this custom server does not replace Next.js. It wraps around it. The same server handles two jobs at once:

  1. It runs the normal Next.js request handler, for all your pages and routes
  2. It also runs the Socket.IO server, attached to the exact same underlying HTTP server

Both share one persistent Node.js process, which stays running continuously instead of spinning up and down per request.

The Trade-off You Must Accept

Using a custom server is not free. It comes with a real trade-off, and you should know this upfront:

  • You lose Automatic Static Optimization for parts of your app that rely on it
  • You lose the ability to deploy on Vercel using its standard serverless deployment, since Vercel's serverless functions cannot hold WebSocket connections open
  • Your app now needs to run on a platform that supports long-running Node.js processes, such as a VPS, Render, Railway, Fly.io, AWS EC2, or a Docker container

Because of this trade-off, some production teams choose a different architecture entirely: keeping Next.js on Vercel as normal, and running the Socket.IO server as a completely separate service (often built with Express), which the Next.js frontend simply connects to as a client. Both approaches are valid. For this course, since our focus is learning Socket.IO itself deeply, we will use the simpler combined custom server approach, where Next.js and Socket.IO share one server.

How the Combined Server Works, Conceptually

Here is the mental model before we see actual code in the next post:

Request comes in
      |
      v
+--------------------+
|   Node.js Server    |
|  (server.ts file)   |
+--------------------+
      |          |
      v          v
  Next.js     Socket.IO
  handles     handles
  pages &     real-time
  routes      events

Both Next.js and Socket.IO listen on the same HTTP server, on the same port. Regular page requests go to Next.js. WebSocket upgrade requests go to Socket.IO. They coexist peacefully on the same process.

Where This File Lives

In the next post, we will create this custom server as a file called server.ts at the root of the project, outside the src/app folder. This file will become the actual entry point of our application from now on, replacing the default next start behavior.

Summary

  • Next.js normally runs on short-lived serverless functions, which cannot hold a Socket.IO connection open
  • Socket.IO needs one continuously running server, since it depends on persistent, long-lived connections
  • The solution is a custom server, a Node.js process that runs Next.js and Socket.IO together on the same HTTP server
  • This approach removes automatic serverless deployment on platforms like Vercel, and requires a host that supports long-running processes
  • Some production apps instead run Socket.IO as a fully separate service, but this course uses the combined custom server approach for simplicity

In the next post, we will actually build this custom server, step by step, using TypeScript.

PHASE 2 — Topic 5: Creating a Fresh Next.js 16 Project with TypeScript and Tailwind CSS

We finished all the theory of Phase 1. Now it's time to get hands-on. In this post, we set up the actual project we will use for the rest of this course.

Requirements Before You Start

Make sure you have these installed on your machine:

  • Node.js, version 20.9 or higher
  • A code editor (VS Code is recommended)
  • A terminal (Command Prompt, PowerShell, or any terminal you prefer)

You can check your Node.js version with:


    node -v

If it shows something below 20.9, update Node.js first before continuing.

Step 1: Create the Project

Next.js provides an official command that scaffolds a complete project for you, including TypeScript and Tailwind CSS, without any manual setup. Run this in your terminal:


    npx create-next-app@latest socket-chat-app

You will be asked a series of questions. Answer them like this:


    Would you like to use TypeScript?        Yes
    Which linter would you like to use?      ESLint
    Would you like to use Tailwind CSS?      Yes
    Would you like your code inside a src/ directory?   Yes
    Would you like to use App Router?        Yes
    Would you like to customize the import alias (@/*)? No
    Would you like to include AGENTS.md?     No

This creates a new folder called socket-chat-app with everything already configured: TypeScript, Tailwind CSS, ESLint, and the App Router.

Step 2: Move Into the Project and Start It


    cd socket-chat-app
    npm run dev

Open your browser at http://localhost:3000. If you see the default Next.js welcome page styled with Tailwind, everything is working correctly.

Step 3: Understanding What Was Created

Here are the important files and folders you will be working with throughout this course:


    socket-chat-app/
    ├── src/
    │   ├── app/
    │   │   ├── layout.tsx      → Root layout, wraps every page
    │   │   ├── page.tsx        → The homepage
    │   │   └── globals.css     → Global styles, Tailwind import
    ├── public/                 → Static files (images, icons)
    ├── package.json            → Project dependencies and scripts
    ├── tsconfig.json           → TypeScript configuration
    ├── next.config.ts          → Next.js configuration

Step 4: How Tailwind Is Set Up (v4)

Next.js 16 ships with Tailwind CSS v4 by default, which works differently from older versions. There is no tailwind.config.js file anymore. Instead, open src/app/globals.css, and you will see something like this:


    @import "tailwindcss";

That single line is enough to enable all of Tailwind's utility classes throughout your project. Any custom theme values (colors, fonts, spacing) can also be defined directly inside this same CSS file using a @theme block, but for this course, the defaults are enough to get started.

Step 5: Confirm TypeScript and Tailwind Are Both Working

Open src/app/page.tsx and replace its content with this:


  export default function Home() {
    return (
      <main className="flex min-h-screen flex-col items-center justify-center bg-gray-900 text-white">
        <h1 className="text-4xl font-bold">
          Next.js + TypeScript + Tailwind CSS
        </h1>
        <p className="mt-4 text-gray-400">
          Setup complete. Ready for Socket.IO.
        </p>
      </main>
    );
  }

Save the file, and check your browser. You should see a dark background, centered white heading, and gray subtext. If the styling appears correctly, both TypeScript and Tailwind are working as expected.

What We Have Not Installed Yet

At this stage, we only have a standard Next.js project. We have not installed Socket.IO yet, and we also have not set up the custom server that Socket.IO requires. That is intentional, because Socket.IO needs a persistent server, which works a bit differently from how Next.js normally runs. We will cover exactly why in the next post, before touching any Socket.IO code.

Summary

  • We created a fresh Next.js 16 project using create-next-app, with TypeScript, Tailwind CSS, ESLint, and the App Router enabled
  • Tailwind CSS v4 no longer needs a config file; it is enabled with a single @import "tailwindcss"; line in globals.css
  • We verified the setup by rendering a simple styled page
  • The project is not yet ready for Socket.IO, since that requires a persistent server, which is the topic of the next post

In the next post, we will understand why Socket.IO needs a persistent server, and how this fits into a Next.js application.

PHASE 1 — Topic 4: How Socket.IO Works Internally (Engine.IO, Transports, and Fallback)

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:

  1. Engine.IO — the low-level layer. It handles the actual connection: opening it, choosing a transport, upgrading it, and keeping it alive.
  2. 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"])
  • pingInterval and pingTimeout values, 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.

PHASE 1 — Topic 3: What Socket.IO Actually Is, and Why It Is Used Instead of Plain WebSockets

In the last post, we learned what WebSockets are, and we also saw that raw WebSockets leave some important gaps. This post covers the tool that fills those gaps: Socket.IO.

What Is Socket.IO?

Socket.IO is a JavaScript library, not a protocol. It has two parts:

  • A server-side library (used with Node.js)
  • A client-side library (used in the browser, or in mobile apps)

Both parts work together and understand a shared format for sending and receiving data. Internally, Socket.IO uses WebSockets whenever possible, but it does not force you to deal with the low-level details of the WebSocket protocol yourself. Instead, it gives you a much simpler, event-based way of communicating.

Important distinction: WebSocket is a protocol. Socket.IO is a library built on top of that protocol, with a lot of extra functionality added.

Why Not Just Use Plain WebSockets?

Plain WebSockets give you a raw, bidirectional connection, and nothing else. That sounds fine at first, but the moment you try to build a real production app, you quickly run into gaps you have to fill yourself:

  • No automatic reconnection if the connection drops
  • No fallback if WebSocket is blocked by a firewall or proxy
  • No built-in way to group clients (like chat rooms)
  • No built-in way to organize different parts of your app on one connection
  • No message acknowledgment system to confirm delivery

Socket.IO exists to solve exactly these problems, so you don't have to build them from scratch.

Key Features Socket.IO Adds on Top of WebSockets

1. Automatic Reconnection Real network connections are unstable. Mobile users switch from Wi-Fi to mobile data, laptops go to sleep, servers restart during deployments. Socket.IO detects when a connection drops and automatically tries to reconnect, using increasing delays between attempts (called exponential backoff), so it does not overwhelm the server.

2. Fallback to HTTP Long-Polling If a WebSocket connection cannot be established for some reason, for example because of a misconfigured proxy or restrictive network, Socket.IO automatically falls back to HTTP long-polling instead. Your application code stays exactly the same. You do not need to know or care which transport is actually being used underneath.

3. Event-Based Communication Instead of just sending raw messages, Socket.IO lets you define your own named events. For example, you can create events like "newMessage", "userTyping", or "orderUpdated", and listen for them separately. This makes your code far more organized compared to manually parsing every incoming message yourself.

4. Rooms Rooms let you group specific clients together, so you can send a message to just that group. For example, everyone inside a particular chat conversation can be placed in the same room, and a message can be broadcast only to them, not to every connected user.

5. Namespaces Namespaces let you split your application logic over a single shared connection. For example, you could have a normal namespace for regular users and a separate /admin namespace for admin-only features, without opening a second connection.

6. Packet Buffering If a client temporarily loses connection, Socket.IO can buffer messages and help maintain continuity once the client reconnects, instead of silently losing data.

7. Heartbeat Mechanism Just like raw WebSockets, Socket.IO also uses a ping/pong style heartbeat internally, so it can detect a broken connection even if neither side explicitly closed it.

A Simple Way to Remember the Difference

WebSocket

Socket.IO

What it is

A protocol

A library built on top of the protocol

Reconnection

You build it yourself

Automatic, built in

Fallback if blocked

None

Falls back to HTTP long-polling

Grouping clients

You build it yourself

Built-in rooms

Organizing app logic

You build it yourself

Built-in namespaces

Communication style

Raw messages

Named events

Is Socket.IO Always the Right Choice?

Not always, and it is worth knowing this honestly. Socket.IO adds some overhead compared to a raw WebSocket connection, because of its extra protocol layer. For applications where you need the absolute lowest latency and full control, some teams prefer using raw WebSockets or lighter libraries instead.

However, for most real-world applications, such as chat apps, live notifications, dashboards, and collaborative features, the reliability and convenience Socket.IO provides is well worth the small extra overhead. This is exactly why it remains one of the most widely used real-time libraries today.

Summary

  • Socket.IO is a library built on top of the WebSocket protocol, with both a server and a client part
  • It solves real production problems that raw WebSockets leave unhandled: reconnection, fallback, rooms, namespaces, and more
  • Communication happens through named events instead of raw messages, making code easier to organize
  • It is not the only option, but for most real-time applications, it remains a reliable and beginner-friendly choice

In the next post, we will look at how Socket.IO actually works internally, covering Engine.IO, transports, and how the fallback mechanism functions behind the scenes.

PHASE 1 — Topic 2: WebSockets Explained in Simple Words

In the last post, we saw why plain HTTP is not good enough for real-time apps. Now let's understand the technology that actually solves this problem: WebSockets.

What Is a WebSocket?

A WebSocket is a communication protocol that creates one single connection between the browser and the server, and keeps that connection open for as long as needed. Once this connection is open, both the client and the server can send messages to each other at any time, without asking permission first.

Think of the difference like this:

  • HTTP is like sending letters back and forth. You send a letter, wait for a reply, and then the conversation pauses until you send another letter.
  • WebSocket is like being on a phone call. Once the call connects, both people can speak whenever they want, without hanging up and redialing every time.

Full-Duplex Communication

The most important word to understand here is full-duplex. It means data can travel in both directions at the same time, independently.

  • The client can send a message while the server is also sending one, at the exact same moment.
  • Neither side has to wait for the other to finish before sending something.

This is very different from HTTP, where only the client is allowed to start a conversation. With WebSockets, the server can push data to the client whenever it wants, without the client asking for it.

How a WebSocket Connection Starts: The Handshake

A WebSocket connection does not begin as something completely new. It actually starts as a normal HTTP request. This is called the handshake.

Here is what happens step by step:

  1. The client sends a normal-looking HTTP request to the server, but with a special header: Upgrade: websocket
  2. The client also sends a Sec-WebSocket-Key, which is a random value used to confirm the server understands the WebSocket protocol
  3. If the server supports WebSockets, it replies with a special response: 101 Switching Protocols
  4. Once this response is received, the connection is officially "upgraded" from HTTP to WebSocket
  5. The same underlying TCP connection stays open, but now it follows WebSocket rules instead of HTTP rules

After this handshake, no more HTTP requests are needed for the rest of the conversation. The connection simply stays open.

Why Does WebSocket Start as HTTP?

This might feel like an odd design choice, so here is the reason: firewalls, proxies, and corporate networks are built around HTTP traffic on ports 80 and 443. If WebSocket used a completely different, unfamiliar connection method, it would get blocked by a lot of networks.

By starting the connection as a normal-looking HTTP request, WebSocket traffic is able to pass through the same infrastructure that already supports the web, and then quietly switches over to its own lightweight protocol.

Messages Are Sent as Frames

Once the connection is open, data is not sent using full HTTP requests anymore. Instead, it is sent using small units called frames.

Frames are important because:

  • They carry a very small header, often just a few bytes, compared to full HTTP headers which can be hundreds of bytes
  • They can carry text data (like JSON) or binary data (like images or files)
  • Multiple frames can combine to form one complete message

This is one of the biggest reasons WebSockets are so efficient. You are no longer repeating heavy HTTP headers every single time you want to send a small piece of data.

Keeping the Connection Alive

Since a WebSocket connection can stay open for a long time, both sides need a way to check if the other side is still there. This is done using small control messages, often called ping and pong.

  • The server (or client) sends a small "ping" frame
  • The other side replies with a "pong" frame
  • If no pong comes back within a certain time, the connection is treated as dead, and it gets closed

This heartbeat mechanism helps detect broken connections early, instead of waiting for something to fail unexpectedly.

What WebSocket Solves From Our Previous Problems

Going back to the problems we discussed with HTTP:

Problem with HTTP

How WebSocket Solves It

Server cannot talk first

Server can send data anytime after the connection is open

Polling wastes resources

No repeated requests are needed at all

Delay is unavoidable

Data is pushed instantly, the moment it is available

Every request carries heavy headers

Frames carry minimal overhead after the handshake

Is WebSocket the Final Answer?

WebSocket solves the core real-time problem very well, but using raw WebSockets directly in a real project comes with its own challenges:

  • If the connection drops, you have to manually write logic to reconnect
  • If WebSocket is blocked by a network or firewall, there is no automatic fallback
  • There is no built-in way to group users into rooms or separate parts of your app
  • You have to build your own system for organizing different types of messages

This is exactly where Socket.IO comes in. It is built on top of WebSockets, but it adds all these missing pieces so you do not have to build them yourself.

Summary

  • A WebSocket is a full-duplex, persistent connection between client and server
  • It starts as a normal HTTP request, then upgrades to the WebSocket protocol using a handshake
  • Once connected, data moves in small, lightweight frames instead of full HTTP requests
  • Ping/pong messages keep the connection alive and detect dead connections
  • WebSocket solves HTTP's real-time problems, but still leaves gaps like reconnection handling and message organization, which Socket.IO fills

In the next post, we will look at what Socket.IO actually is, and why it is used instead of plain WebSockets.

PHASE 1 — Topic 1: What Is Real-Time Communication, and Why Normal HTTP Requests Are Not Enough

Introduction

Before learning Socket.IO, you need to understand a simple question: why do we even need something like Socket.IO? To answer that, we first need to understand how normal web communication works, and where it falls short.

What Is Real-Time Communication?

Real-time communication means data moves between the client (browser) and the server instantly, the moment something changes, without the user having to ask for it again and again.

Think about apps you already use every day:

  • WhatsApp Web, where a message appears on your screen the second someone sends it
  • Live cricket score apps, where the score updates automatically
  • Food delivery apps like Swiggy or Zomato, where you see the delivery rider moving live on the map
  • Google Docs, where you see another person typing in real time

In all these examples, you are not refreshing the page. You are not clicking a "check for updates" button. The data just appears. That is real-time communication.

How Normal HTTP Requests Work

Almost every website you have built so far uses HTTP. HTTP works on a simple pattern called request-response:

  1. The client (browser) sends a request to the server, asking for something
  2. The server processes that request and sends back a response
  3. The connection closes

This is like sending a letter to someone and waiting for their reply. Once you get the reply, the conversation is over. If you want to ask something again, you have to send a brand new letter.

Example in plain words:

  • Browser: "Hey server, give me the latest messages."
  • Server: "Here you go, these are the latest messages."
  • Connection closes.

If you want new messages a few seconds later, the browser has to send another request, get another response, and the connection closes again. This cycle repeats every time.

Why This Model Breaks for Real-Time Apps

HTTP was originally designed for loading documents and web pages, not for constant, instant updates. This creates real problems when you try to use it for real-time features.

Problem 1: The server cannot talk first In HTTP, only the client can start a conversation. The server is not allowed to say "hey, new message arrived" on its own. It can only reply when the client asks. So if something happens on the server side, the client has no way of knowing about it immediately.

Problem 2: Polling wastes resources To fake real-time behavior, many old systems use a trick called polling. The client keeps asking the server "any updates?" every few seconds, even if there is nothing new.

Example: checking every 5 seconds


  setInterval(() => {
    fetch("/messages")
      .then(res => res.json())
      .then(data => console.log(data));
  }, 5000);

This works, but it is wasteful. If 10,000 users are polling every 5 seconds, your server is handling thousands of unnecessary requests, most of which return "nothing new." This wastes bandwidth, server power, and battery on mobile devices.

Problem 3: Delay is unavoidable Even with polling, updates are not truly instant. If a message arrives right after a poll request, the user has to wait until the next poll cycle to see it. This delay makes chat apps feel slow and clunky.

Problem 4: Every request carries extra overhead Every single HTTP request carries full headers, even if the actual data is tiny. Sending these headers again and again, just to ask "anything new?", adds unnecessary load on both the client and the server.

A Slightly Better Old Trick: Long Polling

Before better solutions existed, developers used something called long polling. Here the client sends a request, but instead of the server replying immediately, it holds the connection open until it actually has new data. Once new data is available, the server responds, and the client immediately sends another request to keep the cycle going.

This was better than plain polling because it reduced unnecessary empty responses, but it still relied on repeated HTTP requests behind the scenes, so it was not a true real-time solution. Apps like early Gmail and old Facebook Chat used this technique before WebSockets became common.

What Real-Time Apps Actually Need

For a real real-time experience, we need a different kind of connection, one that:

  • Stays open continuously, instead of closing after every request
  • Allows the server to send data to the client whenever it wants, without waiting for a request
  • Allows both sides to send data at any time, not just one direction
  • Avoids repeating unnecessary requests just to check for updates

This is exactly the gap that WebSockets, and later Socket.IO, were built to fill.

Summary

  • HTTP works on a request-response model: client asks, server answers, connection closes
  • This model is fine for loading pages, but breaks down for instant, continuous updates
  • Polling and long polling were early workarounds, but both are inefficient and still not truly real-time
  • Real-time applications need a persistent, two-way connection where the server can also push data whenever it wants

In the next post, we will look at WebSockets in simple words, and understand exactly how they solve the problems we discussed here.

PHASE 2 — Topic 7: Setting Up a Custom Node.js Server Alongside Next.js for Socket.IO

We understood why Socket.IO needs a persistent server. Now let's actually build it. By the end of this post, your project will run throu...