aslain.dev
0%
01 Hizmetler 02 Hakkımda 03 Projeler 04 Stack 05 Blog 06 İletişim
← Tüm makaleler Discord Bots

Discord Bot Cooldown and Spam Protection Guide

As a Discord bot grows, one of the most common problems is command flooding: a user fires the same command dozens of times per second, and the bot answers every single time. A solid discord bot cooldown system is exactly what solves this; it gives each user a short waiting period before they can run a command again, protecting both your bot and Discord's API limits. In this guide we will build the cooldown logic from scratch, write a working discord.js example, and add extra layers against spam.

Why you need cooldowns and spam protection

A cooldown is not just a "slow the user down" feature; it is a line of defense for your bot's stability. The main reasons:

  • API rate limits: Discord limits how many requests a bot can send. If you send too many messages/replies you get a 429 Too Many Requests response and a temporary block.
  • Resource consumption: Expensive commands such as a database query, an external API call, or image generation can lock up your server under spam.
  • Abuse: Without cooldowns, economy, giveaway, or reward commands get exploited.
  • Channel clutter: Repeated bot replies make channels unreadable.

The cooldown logic: storing a timestamp

Every cooldown system is built on a single idea: store when a user last ran the command and check whether enough time has passed. The pseudocode works like this:

  • The user runs the command.
  • Check whether there is a stored timestamp for that user.
  • If there is and (now - stored) < cooldown: reject and tell them the remaining time.
  • Otherwise: run the command and save a new timestamp.

For storage, memory (a Map) is enough for small bots. If you do not want cooldowns to reset when the bot restarts, use Redis or a database.

A per-command cooldown system with discord.js

In discord.js v14, the common and officially recommended approach for slash commands is to keep a separate Collection per command. Collection extends JavaScript's Map class. You can add an optional cooldown field to your command file:

// commands/ping.js
const { SlashCommandBuilder } = require('discord.js');

module.exports = {
  cooldown: 5, // seconds
  data: new SlashCommandBuilder()
    .setName('ping')
    .setDescription('Shows the bot latency'),
  async execute(interaction) {
    await interaction.reply(`Pong! ${interaction.client.ws.ping}ms`);
  },
};

The actual check happens in the interactionCreate event, before the command runs:

// events/interactionCreate.js
const { Events, Collection } = require('discord.js');

module.exports = {
  name: Events.InteractionCreate,
  async execute(interaction) {
    if (!interaction.isChatInputCommand()) return;

    const command = interaction.client.commands.get(interaction.commandName);
    if (!command) return;

    const { cooldowns } = interaction.client;
    if (!cooldowns.has(command.data.name)) {
      cooldowns.set(command.data.name, new Collection());
    }

    const now = Date.now();
    const timestamps = cooldowns.get(command.data.name);
    const cooldownAmount = (command.cooldown ?? 3) * 1000;

    if (timestamps.has(interaction.user.id)) {
      const expiration = timestamps.get(interaction.user.id) + cooldownAmount;
      if (now < expiration) {
        const expiredTimestamp = Math.round(expiration / 1000);
        return interaction.reply({
          content: `Please wait before reusing this command. You can use it again <t:${expiredTimestamp}:R>.`,
          ephemeral: true,
        });
      }
    }

    timestamps.set(interaction.user.id, now);
    setTimeout(() => timestamps.delete(interaction.user.id), cooldownAmount);

    try {
      await command.execute(interaction);
    } catch (error) {
      console.error(error);
    }
  },
};

Do not forget to define the client.cooldowns collection once at startup: add client.cooldowns = new Collection(); when you set up your client. <t:...:R> is Discord's relative timestamp format and shows the user an auto-updating string such as "in 3 seconds".

User, guild, and global cooldown types

The example above applies a per-user cooldown, which is the most common need. But different scenarios require different keys:

  • Per user: Use interaction.user.id as the key. Ideal for personal commands (profile, balance).
  • Per guild: Use interaction.guildId as the key. Makes sense for commands that affect the whole server (announcements, bulk operations).
  • Per channel: Rate-limit specific channels with interaction.channelId.
  • Global: Use a fixed key (e.g. the command name) for commands that protect an expensive external API.

You can reuse the same logic by only changing which field you store as the key. To combine multiple types, build a composite key: `${interaction.guildId}-${interaction.user.id}`.

Discord's own rate limits (429) and discord.js

Your own cooldown stops users from spamming the bot; but the bot itself is also subject to limits against the Discord API. The good news: the @discordjs/rest layer underneath discord.js automatically manages global and route-based rate limits, queues requests, and waits on a 429. Still, there are points worth knowing:

  • Do not send hundreds of messages inside a loop; batch operations or insert a delay instead.
  • Using ephemeral: true on transient replies (visible only to the person who ran the command) reduces channel clutter and needless message traffic.
  • For long-running work, call interaction.deferReply() first; otherwise you hit the 3-second interaction timeout.
  • If you do hit a global rate limit, listen to the rateLimited event to log it and reduce your request volume.

Extra measures against spam

A cooldown is the first line of defense; against aggressive abuse, add these layers:

  • Escalating penalty: Temporarily increase the wait time for a user who repeatedly hits the cooldown.
  • Watch list: Ignore a user for a short while once they exceed a command-attempt threshold (a simple rate limiter).
  • Permission check: Add an interaction.memberPermissions check to exempt admins from cooldowns.
  • Persistent storage: Keep cooldowns in Redis or a database for reward/economy commands so they cannot be abused after a restart.

Frequently Asked Questions

Is it safe to keep cooldown data in memory?

For small and medium bots, memory (a Collection/Map) is fast and sufficient. However, it resets every time the bot restarts. For abuse-prone commands such as giveaways, rewards, or economy, I recommend storing cooldowns persistently in Redis or a database.

If discord.js handles rate limits for me, why write a cooldown at all?

They serve different purposes. The discord.js REST layer regulates the requests the bot sends to Discord. Your cooldown controls how often users can trigger the bot; that logic prevents spam, resource consumption, and game-mechanic abuse.

Does the same logic apply to discord.py?

Yes, the principle is identical: store the last-used time and check the difference. discord.py also has a built-in @commands.cooldown decorator and BucketType options, which let you define per-user/guild/channel cooldowns in a single line.

Is your bot crumbling under spam? I can help you harden it with command cooldowns, rate-limit handling, and an abuse-resistant architecture. Get in touch and let's talk about your project.

Bu kategorideki tüm yazılar →

Devamı için