How to Create a Discord Poll Bot with Reaction Voting
Learn how to build a custom Discord poll bot using reaction-based voting — step-by-step setup, code examples, and pro tips for reliability & UX.
September 13, 2026 · 2001 views
Creating a Discord poll bot is one of the most practical and rewarding beginner-to-intermediate bot projects you can undertake. Whether you're managing a community server, running educational discussions, or just want real-time feedback from members, a well-designed reaction-based voting system adds interactivity, transparency, and fun 🎯. Unlike third-party poll bots that may lack customization or go offline unexpectedly, building your own poll bot gives you full control over behavior, styling, moderation, and data handling — all while deepening your understanding of Discord’s API and modern JavaScript/Python workflows.
In this comprehensive tutorial, we’ll walk through designing and deploying a lightweight, production-ready Discord poll bot using reactions (✅, ❌, 🟡, etc.) as vote inputs. We’ll focus on Node.js with discord.js v14+ (the most widely adopted framework), but include key Python (discord.py) equivalents where relevant. You’ll learn how to parse commands, create embeds, listen for reactions, validate votes, prevent duplicates, and store results — all while keeping your bot stable, scalable, and user-friendly.
Why Use Reaction-Based Polls?
Reaction-based voting is native to Discord, requires zero extra UI, and feels intuitive to users. Unlike message-based polls (e.g., typing “A” or “Yes”), reaction polls:
- ✅ Are mobile-friendly and accessible
- ✅ Don’t clutter chat history with vote messages
- ✅ Allow anonymous (or opt-in identifiable) participation
- ✅ Support emoji diversity — including custom server emojis
- ✅ Enable instant visual feedback via reaction counts
That said, they do require careful handling: race conditions, reaction removals, permission checks, and edge cases like deleted messages or banned users. This guide helps you avoid those pitfalls.
Prerequisites & Setup
Before writing code, ensure you have:
- A Discord Developer Account (discord.com/developers)
- A new application created in the Developer Portal → Bot tab → copy the token
- Node.js v18.17+ installed locally (check with
node --version) - A text editor (VS Code recommended) and basic terminal fluency
- Optional but recommended: Git, GitHub account, and a free Replit or Railway account for hosting
💡 Pro Tip: Never hardcode your bot token! Use environment variables. Store it in a
.envfile:TOKEN=your_bot_token_here_abc123... CLIENT_ID=123456789012345678Then load it using
dotenv.config()in your entry file.
Step-by-Step: Building the Poll Bot
Step 1: Initialize Your Project & Install Dependencies
Create a new folder and run:
npm init -y
npm install discord.js @discordjs/rest discord-api-types dotenv
npm install --save-dev nodemon
Initialize index.js as your main file and set up basic client scaffolding:
const { Client, GatewayIntentBits, Collection } = require('discord.js');
const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v10');
const dotenv = require('dotenv');
dotenv.config();
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMessageReactions,
],
});
client.commands = new Collection();
client.once('ready', () => {
console.log(`✅ ${client.user.tag} is online and ready for polls!`);
});
client.login(process.env.TOKEN);
Step 2: Register a Slash Command /poll
Add a command to trigger polls. First, define the command structure:
// commands/poll.js
module.exports = {
data: {
name: 'poll',
description: 'Create a new reaction-based poll',
options: [
{
type: 3,
name: 'question',
description: 'Your poll question',
required: true,
},
{
type: 3,
name: 'options',
description: 'Comma-separated options (max 6)',
required: true,
},
{
type: 4,
name: 'duration',
description: 'Poll duration in minutes (default: 60)',
required: false,
},
],
},
async execute(interaction) {
const question = interaction.options.getString('question');
const rawOptions = interaction.options.getString('options').split(',').map(o => o.trim()).slice(0, 6);
const duration = interaction.options.getInteger('duration') || 60;
// Build embed
const pollEmbed = {
color: 0x5865F2,
title: `📊 ${question}`,
description: rawOptions.map((opt, i) => `**${String.fromCodePoint(0x1F1E6 + i)}** ${opt}`).join('\n'),
footer: { text: `Poll ends in ${duration}m • React to vote!` },
timestamp: new Date(),
};
await interaction.deferReply();
const message = await interaction.editReply({ embeds: [pollEmbed] });
// Add reactions
for (let i = 0; i < rawOptions.length; i++) {
const emoji = String.fromCodePoint(0x1F1E6 + i); // 🇦, 🇧, 🇨...
await message.react(emoji);
}
// Store poll metadata for later result collection
const pollId = `${message.guildId}-${message.id}`;
client.polls.set(pollId, {
question,
options: rawOptions,
creator: interaction.user.id,
expiresAt: Date.now() + duration * 60 * 1000,
votes: {}, // { userId: 'A' }
messageId: message.id,
channelId: message.channelId,
});
// Auto-close after duration
setTimeout(async () => {
const poll = client.polls.get(pollId);
if (!poll) return;
const channel = await client.channels.fetch(poll.channelId);
const msg = await channel.messages.fetch(poll.messageId).catch(() => null);
if (!msg) return;
const results = await collectPollResults(client, msg, rawOptions);
await msg.reply({
content: `🗳️ **Final Results for _${question}_**`,
embeds: [buildResultsEmbed(question, rawOptions, results)],
});
client.polls.delete(pollId);
}, duration * 60 * 1000);
},
};
Then register it globally (or per-guild):
// deploy-commands.js
const { REST } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v10');
const fs = require('fs');
const path = require('path');
const dotenv = require('dotenv');
dotenv.config();
const commands = [];
const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const command = require(`./commands/${file}`);
commands.push(command.data.toJSON());
}
const rest = new REST({ version: '10' }).setToken(process.env.TOKEN);
(async () => {
try {
console.log(`Started refreshing ${commands.length} application (/) commands.`);
const data = await rest.put(
Routes.applicationCommands(process.env.CLIENT_ID),
{ body: commands },
);
console.log(`Successfully reloaded ${data.length} application (/) commands.`);
} catch (error) {
console.error(error);
}
})();
Run node deploy-commands.js once to register /poll.
Step 3: Handle Reactions & Prevent Duplicates
Add event listeners in index.js to capture reactions and enforce one-vote-per-user:
client.on('messageReactionAdd', async (reaction, user) => {
if (user.bot) return;
const pollId = `${reaction.message.guildId}-${reaction.message.id}`;
const poll = client.polls.get(pollId);
if (!poll) return;
const emojiName = reaction.emoji.name;
const optionIndex = ['🇦', '🇧', '🇨', '🇩', '🇪', '🇫'].indexOf(emojiName);
if (optionIndex === -1) return;
// One-vote rule
if (poll.votes[user.id]) {
await reaction.users.remove(user);
return;
}
poll.votes[user.id] = String.fromCodePoint(0x1F1E6 + optionIndex);
});
⚠️ Note: To support custom emoji, extend this logic to check
reaction.emoji.idagainst your pre-defined list — and store emoji IDs inpoll.optionsinstead of letters.
Step 4: Build the Results Embed
Here’s a reusable helper function (utils.js) to generate clean, readable results:
function buildResultsEmbed(question, options, results) {
const totalVotes = Object.values(results).length;
const bars = options.map((opt, i) => {
const count = results[`${i}`] || 0;
const pct = totalVotes ? Math.round((count / totalVotes) * 100) : 0;
const bar = '█'.repeat(Math.min(10, Math.floor(pct / 10))) + '░'.repeat(10 - Math.floor(pct / 10));
return `**${String.fromCodePoint(0x1F1E6 + i)} ${opt}** — ${count} vote${count !== 1 ? 's' : ''} (${pct}%)\n${bar}`;
}).join('\n\n');
return {
color: 0x43B581,
title: `✅ Poll Completed: ${question}`,
description: bars || "No votes were cast.",
footer: { text: `Total voters: ${totalVotes}` },
timestamp: new Date(),
};
}
[Image: Screenshot of a finished poll embed with colorful progress bars and vote counts]
Hosting & Reliability Tips
A local bot dies when your laptop sleeps. For 24/7 uptime:
- ✅ Use Railway.app: Free tier supports Node.js, auto-deploys from GitHub, handles env vars & restarts
- ✅ Or Replit: Great for learning; enable “Always On” with a $7/mo Hacker plan
- ✅ Add health checks:
/pingcommand + uptime monitoring via UptimeRobot - ✅ Log errors to a channel or service like Sentry (free tier available)
Also consider adding moderation safeguards:
- Only allow
/pollin designated channels (checkinteraction.channelIdagainst allowlist) - Enforce role restrictions (e.g., only
@Moderatorcan create polls >24h) - Auto-delete expired poll messages after 7 days to reduce clutter
Quick Tips
- 🔄 Always test polls in a sandbox server first — use
guildIdfiltering during dev - 📏 Limit options to ≤6 (to fit regional indicator emojis and keep UI clean)
- 🛑 Block reactions from users who aren’t in the guild (use
reaction.message.guild.members.fetch(user.id)) - 🗂️ Store poll history in a lightweight JSON file or SQLite DB if you need analytics later
- 🌐 For multilingual servers, add an
/poll locale:froption and translate emoji labels - 🔐 Never expose user vote history publicly — only show aggregated totals unless explicitly consented
FAQ
Q: Can I use custom emojis instead of 🇦/🇧?
A: Yes! Replace String.fromCodePoint(...) with your emoji IDs and update message.react() and messageReactionAdd logic to match.
Q: How do I let users edit or cancel their vote?
A: Add a second reaction (e.g., 🔄) to toggle last vote — store timestamps and allow edits within 2 minutes.
Q: Is this compliant with Discord’s Terms of Service?
A: Yes — as long as you don’t spam, auto-react, or mislead users. Always disclose voting rules in your server’s #rules or poll footer.
Q: Where can I find more advanced examples?
A: Check out DiscordCraft, a curated open-source hub for production-grade Discord bot patterns — including poll persistence, dashboard integrations, and webhook syncs.
Final Thoughts
You now have a fully functional, extensible Discord poll bot — built from scratch, reaction-powered, and ready for real-world use 🚀. This isn’t just a tutorial — it’s a foundation. From here, you can add features like:
- Export results to CSV via
/poll export - Anonymous vs. public vote toggles
- Multi-question surveys (chained polls)
- Integration with Google Forms or Airtable
- Role-based access controls and audit logs
The best part? Every line of code teaches you something deeper about Discord’s ecosystem — from intents and rate limits to embed design and state management. Keep iterating, keep sharing, and remember: great communities grow when voices are heard — and your poll bot just made that a little easier. 🌟
Happy coding — and may your votes always be counted! 🗳️✨