Skip to content

Build a Real-Time Polling App with WebSockets (Step by Step)

DodaTech Updated 2026-06-21 7 min read

In this tutorial, you'll learn about Build a Real. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.

Build a real-time polling application using Express, Socket.IO, and Chart.js where multiple users can vote simultaneously and see live chart updates without refreshing the page.

What You'll Build

You'll build a web-based polling app where anyone can create a poll with multiple options, share a link, and watch votes accumulate in real time as a bar chart. When a user votes, the chart animates instantly on every connected client — no page reload needed.

Why Real-Time Polling Matters

Real-time updates create a sense of liveness and engagement. Think of audience Q&A at a conference, live quiz results in a classroom, or instant feedback on a product feature vote. Users stay on the page longer and feel part of something happening right now. The same WebSocket technology powers live file status updates in DodaZIP when compressing large archives.

Prerequisites

Step 1: Project Setup

mkdir polling-app
cd polling-app
npm init -y
npm install express socket.io uuid
mkdir public

Create this project structure:

polling-app/
├── server.js       # Express + Socket.IO backend
├── public/
│   ├── index.html  # Poll list view
│   └── poll.html   # Individual poll view with chart
└── polls.json      # In-memory storage (simple)

Step 2: The Server

The server manages polls, tracks votes, and broadcasts updates to all connected clients via WebSocket.

// server.js
const express = require("express");
const http = require("http");
const { Server } = require("socket.io");
const { v4: uuidv4 } = require("uuid");

const app = express();
const server = http.createServer(app);
const io = new Server(server);

app.use(express.static("public"));
app.use(express.json());

const polls = {};

app.post("/api/polls", (req, res) => {
  const { question, options } = req.body;
  const id = uuidv4();
  polls[id] = {
    id,
    question,
    options: options.map((opt) => ({ text: opt, votes: 0 })),
    total: 0,
  };
  res.json({ id });
});

app.get("/api/polls/:id", (req, res) => {
  res.json(polls[req.params.id] || null);
});

io.on("connection", (socket) => {
  socket.on("vote", ({ pollId, optionIndex }) => {
    const poll = polls[pollId];
    if (!poll) return;
    poll.options[optionIndex].votes++;
    poll.total++;
    io.emit("update", poll);
  });
});

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

Expected output: Server starts on port 3000. Opening http://localhost:3000 serves the static files.

Step 3: The Frontend — Create Poll

<!-- public/index.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Live Polling App</title>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 600px; margin: 40px auto; padding: 0 20px; }
    input, button { font-size: 1rem; padding: 8px 12px; margin: 4px 0; }
    .option-row { display: flex; gap: 8px; align-items: center; }
    .option-row input { flex: 1; }
    button { background: #6366f1; color: white; border: none; border-radius: 6px; cursor: pointer; }
    button:hover { background: #4f46e5; }
  </style>
</head>
<body>
  <h1>Create a Poll</h1>
  <input id="question" placeholder="Your question..." style="width: 100%;">
  <div id="options">
    <div class="option-row"><input placeholder="Option 1"></div>
    <div class="option-row"><input placeholder="Option 2"></div>
  </div>
  <button onclick="addOption()">+ Add Option</button>
  <button onclick="createPoll()" style="margin-left: 8px;">Create Poll</button>

  <script src="/socket.io/socket.io.js"></script>
  <script>
    function addOption() {
      const div = document.createElement("div");
      div.className = "option-row";
      div.innerHTML = '<input placeholder="Option ' + (document.querySelectorAll(".option-row").length + 1) + '">';
      document.getElementById("options").appendChild(div);
    }

    async function createPoll() {
      const question = document.getElementById("question").value;
      const inputs = document.querySelectorAll(".option-row input");
      const options = Array.from(inputs).map((i) => i.value).filter(Boolean);
      const res = await fetch("/api/polls", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ question, options }),
      });
      const { id } = await res.json();
      window.location.href = "/poll.html?id=" + id;
    }
  </script>
</body>
</html>

Step 4: The Frontend — Live Poll View

<!-- public/poll.html -->
<!DOCTYPE html>
<html>
<head>
  <title>Live Poll</title>
  <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  <style>
    body { font-family: system-ui, sans-serif; max-width: 700px; margin: 40px auto; padding: 0 20px; }
    .option { margin: 8px 0; }
    .option button { width: 100%; text-align: left; padding: 12px 16px; font-size: 1rem;
      border: 2px solid #e2e8f0; border-radius: 8px; background: white; cursor: pointer; }
    .option button:hover { border-color: #6366f1; background: #f5f3ff; }
    #total { text-align: center; color: #64748b; margin-top: 16px; }
  </style>
</head>
<body>
  <h1 id="question">Loading...</h1>
  <div id="options"></div>
  <p id="total"></p>
  <canvas id="chart" height="200"></canvas>

  <script src="/socket.io/socket.io.js"></script>
  <script>
    const params = new URLSearchParams(location.search);
    const pollId = params.get("id");
    const socket = io();
    let chart = null;

    async function loadPoll() {
      const res = await fetch("/api/polls/" + pollId);
      if (!res.ok) return (document.body.innerHTML = "<h1>Poll not found</h1>");
      renderPoll(await res.json());
    }

    function renderPoll(poll) {
      if (!poll) return;
      document.getElementById("question").textContent = poll.question;
      document.getElementById("total").textContent = poll.total + " total votes";

      const container = document.getElementById("options");
      container.innerHTML = "";
      poll.options.forEach((opt, i) => {
        const div = document.createElement("div");
        div.className = "option";
        div.innerHTML = `<button onclick="vote(${i})">${opt.text} (${opt.votes})</button>`;
        container.appendChild(div);
      });

      if (chart) chart.destroy();
      chart = new Chart(document.getElementById("chart"), {
        type: "bar",
        data: {
          labels: poll.options.map((o) => o.text),
          datasets: [{ label: "Votes", data: poll.options.map((o) => o.votes),
            backgroundColor: "#6366f1" }],
        },
        options: { responsive: true, animation: { duration: 300 },
          scales: { y: { beginAtZero: true, ticks: { stepSize: 1 } } } },
      });
    }

    function vote(index) {
      socket.emit("vote", { pollId, optionIndex: index });
    }

    socket.on("update", (poll) => {
      if (poll.id === pollId) renderPoll(poll);
    });

    loadPoll();
  </script>
</body>
</html>

Expected output: Create a poll, copy the URL to another browser tab, vote in one tab — the chart animates and vote counts update instantly in the other tab.

Architecture

sequenceDiagram
    participant A as User A
    participant B as User B
    participant S as Socket.IO Server
    participant P as Poll Store

    A->>P: POST /api/polls (create)
    P-->>A: Return poll ID
    A->>S: connect + vote(pollId, 0)
    S->>P: Increment vote
    S->>A: update(poll)
    S->>B: update(poll)
    B->>S: vote(pollId, 1)
    S->>P: Increment vote
    S->>A: update(poll)
    S->>B: update(poll)

Common Errors

1. Socket.IO connection fails with 404 The client-side script must load from /socket.io/socket.io.js served by the server. Make sure server.js creates an http.Server and passes it to new Server(server)Express alone doesn't handle WebSocket upgrades.

2. Votes not syncing across clients Check that io.emit("update", poll) broadcasts to all connected sockets, not just the sender. Using socket.emit sends only to that one client; io.emit sends to everyone.

3. Chart.js not rendering The canvas element must exist before the chart script runs. Place the <script> tag after the <canvas> in the HTML, or wrap in DOMContentLoaded.

4. Poll data lost on server restart This demo stores polls in memory. For persistence, add SQLite or Redis. The polls object resets every time the server restarts.

5. CORS errors in production When serving frontend from a different domain than the server, configure CORS: new Server(server, { cors: { origin: "https://yourdomain.com" } }).

Practice Questions

1. How does Socket.IO differ from raw WebSockets? Socket.IO adds automatic reconnection, fallback to HTTP long-polling, rooms, namespaces, and a simpler event-based API on top of WebSocket.

2. Why does the chart animate without a page refresh? Socket.IO pushes the updated poll data to all connected clients via the update event. The client's socket.on("update", ...) handler calls renderPoll() which updates the Chart.js instance.

3. How could you prevent a user from voting multiple times? Track voted poll IDs in localStorage on the client, or assign a session cookie on the first visit and check it server-side before applying the vote.

4. Challenge: Add poll expiry Add an expiresAt field to each poll. On the server, check expiry before applying votes. On the client, disable voting buttons and show "Poll closed" when expired.

5. Challenge: Live percentage display Modify the option rendering to show percentages alongside raw vote counts (67%), updated in real time as votes come in.

FAQ

How do I deploy a Socket.IO app?

Use a Process manager like PM2. Behind NGINX, enable proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; for WebSocket support. Platforms like Render and Railway support WebSocket natively.

Can I scale Socket.IO across multiple servers?

Yes. Use the Socket.IO Redis adapter (@socket.io/redis-<a href="/design-patterns/adapter/">adapter</a>) to broadcast messages across all instances via Redis pub/sub.

How do I add authentication?

Use Socket.IO middleware to verify tokens on connection. Example: io.use((socket, next) => { const token = socket.handshake.auth.token; /* verify */ next(); }).

Next Steps

  • Add vote persistence with SQLite so data survives restarts
  • Explore WebSocket security — Rate Limiting and origin validation
  • Try building the Real-Time Chat App project for another WebSocket use case
  • Learn about scaling with Redis pub/sub in the Redis tutorial

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro