---
source_url: https://www.pubnub.com/docs/use-cases/sports-media-entertainment/live-polls
title: Run a live poll during a match
updated_at: 2026-09-30T07:20:08.000Z
---

# Run a live poll during a match

## Documentation index

To discover more PubNub resources:

1. Fetch [PubNub's llms.txt](https://www.pubnub.com/llms-full.txt) for a list of available pages in Markdown format.
2. Identify relevant URLs from that index.
3. Fetch the target pages.

Do not assume a path exists, always check the index first.

A live poll needs three things to work end to end. It needs a way to announce that a poll is open, a way for a fan to vote, and a way to publish the tally. PubNub [Pub/Sub](https://www.pubnub.com/docs/pub-sub/overview.md) carries all three as messages on three separate channels.

A poll and its final tally both have to survive being read after the fact, so storing them requires enabling [Message Persistence](https://www.pubnub.com/docs/data-storage/message-history/overview.md) on your keyset. In this tutorial, you write a Node.js script that opens a poll on `game.new-poll` and a second script that receives the poll and casts a vote on `game.poll-votes`. Both scripts then watch a tally arrive on `game.poll-results`.

## What you'll build

`open-poll.js` publishes a poll to `game.new-poll` and asks PubNub to store it, so a fan who starts late can still fetch it from Message Persistence. `vote.js` receives that poll, then publishes the fan's vote to `game.poll-votes`, a channel the fan only writes to. After 20 seconds, `open-poll.js` stands in for a server and publishes the tally to `game.poll-results`, and both scripts receive it.

```mermaid
sequenceDiagram
    participant Open as open-poll.js
    participant NewPoll as game.new-poll
    participant Votes as game.poll-votes
    participant Results as game.poll-results
    participant Vote as vote.js

    Open->>NewPoll: publish poll, stored
    NewPoll->>Vote: poll arrives, or fetched from Message Persistence
    Vote->>Votes: publish vote
    Open->>Results: after 20 s, publish tally
    Results->>Open: tally
    Results->>Vote: tally
```

## Before you begin

You need:

* Node.js 22 or later.
* A PubNub account and your own keyset. If you don't have one, follow [Set up your account](https://www.pubnub.com/docs/architecture/authentication/set-up-your-account.md) to create one.
* Message Persistence enabled on that keyset. Open the [Admin Portal](https://admin.pubnub.com), select your keyset, and turn on **Message Persistence** under its settings.

This tutorial uses three channels: `game.new-poll`, `game.poll-votes`, and `game.poll-results`.

## Set up the project

Create a new directory and install the PubNub SDK:

```bash
mkdir live-polls-tutorial
cd live-polls-tutorial
npm init -y
npm install pubnub
```

This tutorial's code uses `import`, so add `"type": "module"` to the `package.json` `npm init -y` created.

Create two files: `open-poll.js`, which announces a poll and shows the tally, and `vote.js`, which receives a poll and casts a vote.

Add this to `open-poll.js`:

import PubNub from 'pubnub';
const pubnub = new PubNub({  publishKey: 'YOUR_PUBLISH_KEY',  subscribeKey: 'YOUR_SUBSCRIBE_KEY',  userId: 'match-service',});

Add this to `vote.js`:

import PubNub from 'pubnub';
const pubnub = new PubNub({  publishKey: 'YOUR_PUBLISH_KEY',  subscribeKey: 'YOUR_SUBSCRIBE_KEY',  userId: 'fan-42',});

Replace `YOUR_PUBLISH_KEY` and `YOUR_SUBSCRIBE_KEY` in both files with the keys from your keyset. `match-service` identifies the script that opens polls and tallies votes, and `fan-42` identifies the fan casting a vote.

## Give each stage of the poll its own channel

A fan only needs to read `game.new-poll` and `game.poll-results`. That same fan needs to write to `game.poll-votes`, but never read it, because reading it would let one fan see how others voted before the tally closes. Splitting the poll's three stages across three channels lets [Access Manager](https://www.pubnub.com/docs/security/access-control/permission-model.md) express that difference. A single shared channel can't grant read to two of its uses and write to a third.

| Channel | Carries |
| --- | --- |
| `game.new-poll` | A poll opening |
| `game.poll-votes` | One fan's vote |
| `game.poll-results` | A poll tally |

```mermaid
flowchart TB
    SVC["<b>Poll service</b><br/>opens the poll · counts votes<br/>publishes the tally"]
    NEW["<b>game.new-poll</b><br/>the question"]
    RES["<b>game.poll-results</b><br/>the running tally"]
    FAN["<b>Fan client</b><br/>reads two channels<br/>writes one"]
    VOTES["<b>game.poll-votes</b><br/>one message per vote"]

    SVC --> NEW --> FAN
    SVC --> RES --> FAN
    FAN --> VOTES --> SVC
```

Only the poll service reads `game.poll-votes`. It counts the votes and publishes the tally to `game.poll-results`, so no fan sees a running count before the poll closes.

This tutorial doesn't configure Access Manager, so you can focus on the poll itself. When you do add it, grant `read` on `game.new-poll` and `game.poll-results` and `write` on `game.poll-votes` to each fan's token.

## Open a poll

Add this to `open-poll.js`:

```javascript
const poll = {
  id: 'poll-1',
  title: 'Who will win the match?',
  durationSeconds: 60,
  options: [
    { id: 1, text: 'Home team' },
    { id: 2, text: 'Away team' },
    { id: 3, text: 'Draw' },
  ],
};

try {
  const response = await pubnub.publish({
    channel: 'game.new-poll',
    message: poll,
    customMessageType: 'poll-opened',
    storeInHistory: true,
  });
  console.log('poll published at timetoken:', response.timetoken);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(`Publishing the poll failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
```

`storeInHistory: true` tells PubNub to keep this message in Message Persistence, so a fan who arrives after the poll opened can still retrieve it. `customMessageType` labels the message `poll-opened` so a subscriber can branch on the label without inspecting the payload. For more on both parameters, refer to [Send different message types](https://www.pubnub.com/docs/pub-sub/publish/send-different-message-types.md).

The standard message payload size limit is 32 KiB. This includes the channel name and any metadata. A poll with a long title or many options can approach that limit, and PubNub rejects a publish that exceeds it.

## Show the poll to fans

Add this to `vote.js`:

```javascript
const pollChannel = pubnub.channel('game.new-poll');
const pollSubscription = pollChannel.subscription({ receivePresenceEvents: false });

pollSubscription.onMessage = (event) => {
  console.log('new poll:', event.message);
};

pollSubscription.subscribe();
```

`pubnub.channel()` creates a channel entity, and `.subscription()` scopes a subscription to it that fires `onMessage` for every poll published on `game.new-poll`. For the full entity and subscription model, refer to [Subscriptions and subscription sets](https://www.pubnub.com/docs/pub-sub/subscribe/subscriptions.md).

## Catch fans who arrive after the poll opened

A fan who starts `vote.js` after the poll already went out never receives the `livePollsSubscribeToPolls` message, because a subscription only delivers events published after it connects. This is why Message Persistence matters. The poll you stored with `storeInHistory: true` is still retrievable. Add this to `vote.js`:

```javascript
try {
  const response = await pubnub.fetchMessages({
    channels: ['game.new-poll'],
    count: 1,
  });

  const entries = response.channels['game.new-poll'];

  if (entries && entries.length > 0) {
    console.log('poll that is already open:', entries[0].message);
  }
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(`Fetching the open poll failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
```

`fetchMessages` with `count: 1` retrieves only the most recent stored message on `game.new-poll`, which is the currently open poll if one exists. For pagination, time ranges, and other retrieval options, refer to [Fetch missed messages](https://www.pubnub.com/docs/data-storage/message-history/retrieve-message-history.md#fetch-missed-messages).

## Collect votes

Add this to `vote.js`, using whichever option ID the fan picked:

```javascript
try {
  const response = await pubnub.publish({
    channel: 'game.poll-votes',
    message: { pollId: 'poll-1', optionId: 2 },
    customMessageType: 'poll-vote',
  });
  console.log('vote published at timetoken:', response.timetoken);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(`Publishing the vote failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
```

The vote payload carries no field naming the fan who cast it. PubNub already records the publisher of every message, so adding one would be redundant. On the receiving side, the JavaScript SDK exposes that value as `event.publisher`.

## Count the votes and publish the tally

Counting votes and publishing the result belongs on a server or in a [PubNub Function](https://www.pubnub.com/docs/message-processing/serverless/create-function.md), never in the fan's own client. A fan's client runs code you shipped to them, so any one-vote-per-fan rule you write into that client is a rule the fan can edit out before it runs. Only code that executes somewhere the fan can't change, your server or a Function, can enforce it.

For this tutorial, add the tally to `open-poll.js` itself, on a delay, so you can watch the whole flow without standing up a separate server:

```javascript
setTimeout(async () => {
  // The results publish goes here.
}, 20000);
```

Inside that timeout, add the results publish:

```javascript
try {
  const response = await pubnub.publish({
    channel: 'game.poll-results',
    message: {
      pollId: 'poll-1',
      totals: { 1: 812, 2: 1043, 3: 219 },
      closed: true,
    },
    customMessageType: 'poll-results',
    storeInHistory: true,
  });
  console.log('results published at timetoken:', response.timetoken);
} catch (error) {
  const status = error instanceof Error && 'status' in error ? error.status : undefined;
  console.error(`Publishing the results failed: ${error}${status ? ` Additional information: ${status}` : ''}`);
}
```

Add this to both `open-poll.js` and `vote.js`, so the poll opener and the fan both see the tally arrive:

```javascript
const resultsChannel = pubnub.channel('game.poll-results');
const resultsSubscription = resultsChannel.subscription({ receivePresenceEvents: false });

resultsSubscription.onMessage = (event) => {
  console.log('poll results:', event.message);
};

resultsSubscription.subscribe();
```

Add the results subscription to `open-poll.js` before the `setTimeout` call, so it's already active when the delayed publish fires.

## Run it

Open two terminals in the project directory. In the first, run:

```bash
node open-poll.js
```

You should see:

```text
poll published at timetoken: 17123456789012345
```

Within 20 seconds, in the second terminal, run:

```bash
node vote.js
```

You should see:

```text
poll that is already open: { id: 'poll-1', title: 'Who will win the match?', durationSeconds: 60, options: [ { id: 1, text: 'Home team' }, { id: 2, text: 'Away team' }, { id: 3, text: 'Draw' } ] }
vote published at timetoken: 17123456789045678
```

After the 20-second delay, both terminals print the tally:

```text
poll results: { pollId: 'poll-1', totals: { '1': 812, '2': 1043, '3': 219 }, closed: true }
```

The `open-poll.js` terminal also prints the line that triggered it:

```text
results published at timetoken: 17123456789098765
```

Both scripts keep running because their subscriptions hold the connection open. Press **Ctrl+C** in each terminal to stop them.

## What happened

1. `open-poll.js` published a poll to `game.new-poll` and asked PubNub to store it.
2. `vote.js` started after that publish, so its live subscription to `game.new-poll` never saw the poll directly.
3. `vote.js` fetched the stored poll from Message Persistence instead, which is why it saw the poll even though it started late.
4. `vote.js` published a vote to `game.poll-votes`, a channel it only ever writes to.
5. `open-poll.js` published a tally to `game.poll-results` after a delay that stood in for a server's counting step.
6. Both scripts, subscribed to `game.poll-results`, received the same tally.

## Next steps

* [Open a poll automatically when fans react](https://www.pubnub.com/docs/use-cases/sports-media-entertainment/automated-polling.md). Trigger this same poll-opening step from a reaction stream instead of a script you run by hand.
* [Message Persistence](https://www.pubnub.com/docs/data-storage/message-history/overview.md). What gets stored, how retention works, and how timetokens tie storage to live delivery.
* [Access Manager](https://www.pubnub.com/docs/security/access-control/overview.md). Issue the read and write tokens this tutorial's channel split is designed for.
* [PubNub Functions](https://www.pubnub.com/docs/message-processing/serverless/overview.md). Run the vote-tallying step at the edge instead of on your own server.

Last updated at: 2026-09-30T07:20:08.000Z
