When a Discord bot responds to every command instantly without any delay, users can spam commands and overwhelm the bot’s resources. A cooldown system prevents this by enforcing a waiting period between commands for each user. This article explains how to implement per-user cooldown limits in a Discord bot using Discord.js v14 and Node.js. You will learn the core concept of cooldowns, see a working code example, and understand how to avoid common pitfalls like memory leaks and incorrect time calculations.
Key Takeaways: Implementing Per-User Cooldowns in Discord Bots
- Collection-based cooldown map: Use a
MaporCollectionto store each user’s last command timestamp. - Timestamp comparison: Subtract the stored timestamp from
Date.now()and compare against your cooldown duration in milliseconds. - Error reply with remaining time: Send a message showing how many seconds remain before the user can use the command again.
How a Per-User Cooldown Works in Discord.js
A per-user cooldown limits how often a single user can run a specific command. The bot stores the time when a user last executed a command. When the user runs the same command again, the bot checks if enough time has passed. If not, the bot refuses the command and tells the user how long to wait.
The most common approach uses a JavaScript Map or Discord.js Collection object. The key is the user ID, and the value is a timestamp in milliseconds. When a command runs, the bot stores Date.now(). On the next invocation, the bot compares Date.now() with the stored value. If the difference is less than the cooldown duration, the command is blocked.
This method works for both slash commands and prefix commands. For slash commands, the cooldown check runs inside the execute function. For prefix commands, the check runs inside the messageCreate event handler. The same logic applies to both.
Prerequisites
Before implementing cooldowns, make sure you have:
- Node.js version 16.9.0 or higher installed
- A Discord bot application created in the Discord Developer Portal
- Discord.js v14 installed (
npm install discord.js@14) - Basic understanding of asynchronous JavaScript and event handling
Steps to Implement Per-User Cooldowns in a Slash Command
The following steps show how to add a 10-second cooldown to a slash command. The bot stores cooldowns in a global Collection and checks timestamps before executing the command logic.
- Create a new Collection in your main bot file
Add this line at the top of yourindex.jsor main bot entry file:const cooldowns = new Discord.Collection();. This collection will store user IDs and their cooldown timestamps for every command. - Define the cooldown duration in milliseconds
Inside your command handler or directly in the command file, set a constant:const cooldownDuration = 10000;. This equals 10 seconds. Adjust this number as needed for your use case. - Check if the user already has a cooldown entry
Inside theexecutefunction of your slash command, useif (!cooldowns.has(commandName)) cooldowns.set(commandName, new Discord.Collection());. This ensures each command has its own cooldown collection. - Get the current timestamp and the stored timestamp for the user
Write:const now = Date.now();andconst timestamps = cooldowns.get(commandName);. Then get the user’s stored timestamp:const cooldownAmount = cooldownDuration;andconst userTimestamp = timestamps.get(interaction.user.id);. - Compare timestamps and block if still on cooldown
Write:if (userTimestamp) { const expirationTime = userTimestamp + cooldownAmount; if (now < expirationTime) { const timeLeft = (expirationTime - now) / 1000; return interaction.reply({ content: `Please wait ${timeLeft.toFixed(1)} seconds before using this command again.`, ephemeral: true }); } }. This sends a private reply showing the remaining time. - Set the new timestamp after the command runs
After your command logic executes, write:timestamps.set(interaction.user.id, now);. This updates the cooldown so the user must wait again. Optionally add a timeout to clean up old entries:setTimeout(() => timestamps.delete(interaction.user.id), cooldownAmount);.
After following these steps, the slash command will refuse execution if the user runs it more than once every 10 seconds.
Steps to Implement Per-User Cooldowns in a Prefix Command
Prefix commands work differently because they rely on the messageCreate event. The cooldown logic is similar, but you use message.author.id instead of interaction.user.id.
- Create the cooldown Collection outside the event handler
In your main bot file, add:const cooldowns = new Discord.Collection();. This collection stores timestamps for each command name. - Inside the messageCreate event, check the command prefix
Write:if (!message.content.startsWith(prefix) || message.author.bot) return;. Extract the command name and arguments as usual. - Initialize the cooldown collection for this command
Write:if (!cooldowns.has(commandName)) cooldowns.set(commandName, new Discord.Collection());. - Get timestamps and check cooldown
Write:const now = Date.now(); const timestamps = cooldowns.get(commandName); const cooldownAmount = 10000; const userTimestamp = timestamps.get(message.author.id);. Then compare:if (userTimestamp) { const expirationTime = userTimestamp + cooldownAmount; if (now < expirationTime) { const timeLeft = (expirationTime - now) / 1000; return message.reply(`Please wait ${timeLeft.toFixed(1)} seconds before using this command again.`); } }. - Set the new timestamp and optionally clean up
Write:timestamps.set(message.author.id, now); setTimeout(() => timestamps.delete(message.author.id), cooldownAmount);.
Now your prefix command also enforces a 10-second per-user cooldown.
Common Mistakes and Limitations to Avoid
Cooldown Does Not Persist After Bot Restart
The in-memory Collection is lost when the bot restarts. Users can immediately run commands again after a restart. To persist cooldowns across restarts, store the timestamps in a database like SQLite or Redis. For most small bots, the in-memory approach is sufficient.
Cooldown Applies to All Commands Instead of Per Command
If you use a single collection for all commands, a user's cooldown for one command blocks all other commands. The fix is to nest collections: one collection per command name, each containing user timestamps. The code examples above already implement this correctly.
Time Calculation Errors Due to Server Clock Drift
Date.now() uses the server's system clock. If the clock drifts or is manually changed, cooldowns may expire early or late. Use process.hrtime.bigint() for monotonic time if you need high accuracy. For most bots, Date.now() is acceptable.
Memory Leak from Unused Cooldown Entries
If you never delete old cooldown entries, the Collection grows indefinitely. Always use setTimeout to delete the entry after the cooldown expires. The example above includes this cleanup step.
Comparison of Cooldown Storage Methods
| Storage Method | In-Memory Collection | Database (SQLite / Redis) |
|---|---|---|
| Persistence | Lost on restart | Survives restart |
| Performance | Very fast, no I/O | Slower due to I/O |
| Complexity | Simple, no external dependencies | Requires database setup and queries |
| Use case | Small bots with low traffic | Large bots or cross-server cooldowns |
You can implement per-user cooldowns using the in-memory Collection approach covered in this article. The cooldown system uses a map of user IDs to timestamps, compares them on each command invocation, and blocks the command until the wait time expires. To take the next step, add a configurable cooldown duration per command using a JSON file or database. For advanced use, consider implementing a global cooldown that applies across multiple servers or commands using Redis.