Building a Real-Time Polling Application with WebSocket: A Step-by-Step Developer’s Guide

In the modern web landscape, users expect instant feedback and live updates without manually refreshing a page. Traditional polling—where the client repeatedly requests data from the server at set intervals—works but is inefficient and creates unnecessary network load. WebSocket, on the other hand, provides a persistent, full-duplex communication channel between the client and server, making it the perfect technology for real-time features like live voting, collaborative editing, and multiplayer games. In this comprehensive tutorial, we will build a fully functional polling application from scratch using WebSocket technology. You will learn how to set up a Node.js WebSocket server, manage multiple polls and votes in real time, design a clean client interface, and handle essential concerns like broadcasting updates and reconnection. By the end of this guide, you will have a reusable, scalable foundation for any real-time feature you wish to implement. Whether you are a seasoned developer looking to refresh your real-time skills or a beginner eager to dive into WebSocket, this tutorial covers every detail with thorough explanations, code snippets, and best practices.

Before we dive into the code, it is important to understand why WebSocket is the ideal choice for a polling app. In a typical HTTP-based polling system, the client sends a GET request every few seconds to check for new data, even when nothing has changed. This wastes bandwidth and server resources, and more importantly, introduces latency that can be anywhere from hundreds of milliseconds to seconds. WebSocket eliminates this inefficiency by establishing a single TCP connection that stays open. Both the server and client can push messages instantly. For a polling application—where you want to see vote counts update the moment someone casts a vote—WebSocket provides a seamless, near-instantaneous experience. Additionally, WebSocket messages are lightweight compared to HTTP headers, making it ideal for high-frequency updates. In this tutorial, we will use the `ws` library on the server side (Node.js) because it is lightweight and low-level, giving you full control over the message format. On the client side, we will use the built-in WebSocket API available in all modern browsers. We will also store voting data in memory for simplicity, but the same architecture can be extended to use a database or a publish/subscribe system like Redis for horizontal scaling.

Article illustration

Step 1: Setting Up the Project Structure and Dependencies

The first step in any software project is to create a clean, organized directory structure and install the necessary dependencies. We will build a Node.js application with a simple HTTP server that also handles WebSocket connections. Although we could use a framework like Express, we will keep things minimal to focus on the WebSocket logic. Create a new directory for your project, for example `websocket-polling-app`, and navigate into it. Initialize a Node.js project by running `npm init -y` in your terminal. This will generate a `package.json` file with default values. Next, install the `ws` library by running `npm install ws`. The `ws` library is a simple, fast, and well-tested WebSocket implementation for Node.js. No other dependencies are needed for the server side because Node.js includes the `http` module natively. For the client side, we will create a static HTML file served by the same Node.js server. To serve static files, we will use Node’s built-in `fs` (file system) module. Alternatively, you could use a separate web server like Nginx or a framework like Express, but our approach keeps the tutorial self-contained. Your project folder should now contain the following files: `package.json`, `node_modules/`, and later we will create `server.js` for the backend and `client.html` for the frontend. Create an empty `server.js` file and a `client.html` file. We will also create a `public` folder if you prefer, but for simplicity we will serve the HTML from the root. With the structure in place, we can move to the actual server code.

Step 2: Creating the WebSocket Server with Node.js

Now we will write the server script that combines an HTTP server for serving the static client file and a WebSocket server for real-time communication. Open `server.js` and require the `http`, `fs`, and `ws` modules. Start by creating an HTTP server using `http.createServer`. Inside the request handler, check the URL. If the request is for the root path (`/`), read the `client.html` file using `fs.readFile` and send it back with the correct `Content-Type` header. For any other request, return a 404 status. This simple approach allows us to test the application without a separate static file server. After setting up the HTTP server, create a WebSocket server instance by passing the HTTP server to `new WebSocket.Server({ server })`. This attaches the WebSocket server to the same port as the HTTP server, typically port 8080 or 3000. Listen on the desired port and log a message to indicate the server is running. For the WebSocket server, we need to handle the `connection` event. When a client connects, a WebSocket object is created. We will set up event listeners for `message` and `close` on that object. Additionally, we will maintain a set of all connected clients so that we can broadcast messages to everyone. Use a `Map` or a simple array to store clients. For each new connection, add the client to the set and send a welcome message or the current state of all polls. At this point, your server is ready to accept WebSocket connections, but it does not yet handle poll-specific logic. We will implement that in the next step.

Server Code Outline

const http = require('http');
const fs = require('fs');
const WebSocket = require('ws');

const server = http.createServer((req, res) => {
  if (req.url === '/') {
    fs.readFile('client.html', (err, data) => {
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end(data);
    });
  } else {
    res.writeHead(404);
    res.end('Not found');
  }
});

const wss = new WebSocket.Server({ server });

wss.on('connection', (ws) => {
  // ... handle connection
});

server.listen(8080, () => {
  console.log('Server running on http://localhost:8080');
});

Step 3: Implementing the Polling Logic (Create Poll, Vote, Tally Results)

With the WebSocket server skeleton in place, we need to define the data structures and message protocols that will govern our polling app. We will store polls in an in-memory JavaScript object (or Map) where each poll has a unique ID, a question, an array of options, and a vote count for each option. To keep things organized, we will define a set of message types that the client and server can exchange. All messages will be JSON strings. The server will recognize the following types: `create_poll`, `vote`, `get_polls`. When a client sends a `create_poll` message, the server will generate a unique ID (using a simple incrementing counter or a UUID), store the poll, and broadcast the new poll to all connected clients. When a client sends a `vote` message containing a poll ID and an option index, the server will update the vote count for that option and broadcast the updated poll data to everyone. When a client sends a `get_polls` message (typically on initial connection), the server will send the full list of existing polls back to that specific client. Broadcasting is crucial because it ensures that every participant sees the same real-time view. To broadcast, we iterate over the set of connected WebSocket clients and send the JSON stringifyed message using `ws.send()`. However, we must be careful not to send messages to the client that triggered the update, because they already have the optimistic state. We can either let the client update its own UI optimistically or include the sender in the broadcast and let them ignore the echo. For simplicity, we will broadcast to all clients, including the sender, but the sender’s UI will be updated anyway (it is idempotent). The server should also validate that the poll exists and that the vote option index is within bounds. In a production app, you would also prevent duplicate votes from the same user via authentication or a cookie, but we will skip that for now.

Below is a sample data structure for a poll:

{
  id: 1,
  question: "What is your favorite programming language?",
  options: [
    { text: "JavaScript", votes: 10 },
    { text: "Python", votes: 8 },
    { text: "Go", votes: 5 }
  ],
  totalVotes: 23
}

When a vote arrives, we increment the `votes` property of the relevant option and recalculate `totalVotes`. Then we broadcast the updated poll object to all clients. The client will update its DOM accordingly. This pattern is straightforward and works well for a small to medium number of polls and concurrent users. To handle multiple polls, we simply store them all in an object keyed by ID. The server-side code will look something like this: on each `connection`, we set up a message handler that parses the JSON, determines the type, and calls the appropriate function. Remember to handle errors gracefully—if the JSON is malformed, send an error message back to the client.

Step 4: Building the Client-Side HTML and JavaScript

Now let’s switch to the frontend. The client will be a single HTML page that contains a form for creating a new poll and a list of existing polls with voting buttons. We will use plain HTML, CSS, and vanilla JavaScript to keep the focus on WebSocket integration. Start by creating a basic HTML5 document with a title, some styling (inline or separate), and a `

` where the polls will be rendered. Also include a `

` with an input for the question and a textarea for options (each option on a new line). Add a submit button. In the JavaScript section (inside `