Only show guides for your app
Stop one person spamming an expensive command. Build cooldowns two ways — an in-memory Map that's three lines, and a Redis version that survives restarts and spans multiple processes — with the code for both.
Some commands shouldn't run every second — a daily reward, an expensive API call, anything that would be abused by mashing. A cooldown limits how often each user may run a command. This recipe builds it two ways: a dead-simple in-memory Map for a single bot, and a Redis version for when the cooldown must outlive restarts or be shared across processes.
| At a glance | |
|---|---|
| You need | a working bot from the discord.js guide; for the Redis half, a Redis server |
| Plan | free or premium — free runs while your session timer lasts |
| Time | about fifteen minutes |
Keep a Map from command:user to the timestamp when they're allowed again. One function does everything — it returns the seconds still to wait, or 0 if the user is clear (and starts a fresh cooldown):
const cooldowns = new Map(); // "command:userId" -> epoch ms when it's free again
function checkCooldown(command, userId, seconds) {
const key = `${command}:${userId}`;
const now = Date.now();
const until = cooldowns.get(key);
if (until && now < until) {
return Math.ceil((until - now) / 1000); // still cooling down: seconds left
}
cooldowns.set(key, now + seconds * 1000);
return 0; // clear — cooldown now started
}
Wire it into a command. A non-zero return means "block and tell them how long":
client.on(Events.InteractionCreate, async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== 'daily') return;
const left = checkCooldown('daily', interaction.user.id, 60 * 60 * 24); // 24h
if (left > 0) {
const hours = Math.ceil(left / 3600);
await interaction.reply({ content: `⏳ Already claimed. Try again in ~${hours}h.`, ephemeral: true });
return;
}
await interaction.reply('🎁 You claimed your daily reward!');
});
That's a complete, working cooldown. The key includes the command name so /daily and /work cool down independently, and the user ID so each person has their own clock.
💡 Tip: Put the cooldown check first, before any real work. Return early when
left > 0so the expensive part never runs for someone who's rate-limited.
The Map lives in your bot's memory, and that's its ceiling:
Map and its own view of the cooldown.For a short anti-spam cooldown on a single bot, the Map is the right call — simple and instant. When the cooldown is long or must be shared, move it to Redis.
Redis stores the cooldown outside your bot, so a restart doesn't clear it, and every process sees the same clock. It's a natural fit because Redis keys can expire themselves — set a key with a time-to-live and it vanishes on its own when the cooldown ends.
First run a Redis server and connect to it — Caching with Redis covers both. Keep the connection details in .env:
const { createClient } = require('redis');
const client = createClient({
url: `redis://:${process.env.REDIS_PASSWORD}@${process.env.REDIS_HOST}:${process.env.REDIS_PORT}`,
});
client.on('error', (err) => console.error('Redis error:', err));
The cooldown check uses TTL (time-to-live). If the key still exists, its remaining TTL is the seconds left; if it's gone, the user is clear and you set a fresh key with EX:
// seconds remaining (>0 = still cooling down), or 0 if clear (and starts the cooldown)
async function checkCooldown(command, userId, seconds) {
const key = `cooldown:${command}:${userId}`;
const ttl = await client.ttl(key);
if (ttl > 0) return ttl;
await client.set(key, '1', { EX: seconds });
return 0;
}
Same shape as the in-memory function, so the command handler barely changes — just await the check because it talks to Redis:
const left = await checkCooldown('daily', interaction.user.id, 60 * 60 * 24);
if (left > 0) { /* ...block as before... */ }
{ EX: seconds } is what makes this clean: Redis deletes the key when the cooldown expires, so there's no cleanup code and no growing Map — the cooldown simply stops existing when it's over.
⚠️ Heads up: The Redis cooldown is only as available as the Redis server. On a free plan that server stops when its session timer runs out, and cooldown checks start erroring. Keep Redis on an always-on server if real behaviour depends on it — see Keeping apps online. If the app and Redis are both your servers, connect over the private Internal Network and never expose Redis publicly.
In-memory Map |
Redis | |
|---|---|---|
| Setup | none — a variable | a Redis server to run |
| Survives a restart | no | yes |
| Shared across processes | no | yes |
| Best for | short anti-spam, single bot | long cooldowns, sharded/multi-process bots |
| Cleanup | manual (grows until restart) | automatic (keys expire) |
Run the command twice in a row. The first goes through; the second is blocked with the "try again" message. In the Redis version, restart the bot between the two runs — the cooldown still holds, which is the whole point. (The in-memory version resets on that restart.)
left > 0, or the key differs between calls. Log the key and make sure command name and user ID are stable.Map, or the Redis server itself restarted/expired. Confirm the handler awaits the Redis checkCooldown.NOAUTH / WRONGPASS — the Redis URL is missing the password. The shape is redis://:PASSWORD@host:port — note the leading colon.Map grows forever — expected; it only clears on restart. For long-lived bots with many one-off cooldowns, prefer Redis so keys expire themselves.