Skip to content

Using rate limiters in commands

In this page, we'll focus on slash commands, but it works the same for text commands and context menu commands.

Annotated commands

Using an (anonymous) rate limiter

Use @RateLimit or @Cooldown to define one on an application command, it will define a rate limiter automatically, but cannot be referenced anywhere else.

@Command
class SlashRateLimit {

    // A rate limit for this slash command only
    @RateLimit(
        // Apply to each user, regardless of channel/guild
        scope = RateLimitScope.USER,
        // Delete the message telling the user about the remaining rate limit after it expires
        deleteOnRefill = true,
        // At least one of those needs to be empty for the command to be rejected
        bandwidths = [
            // 5 uses, 5 tokens gets added gradually over an hour (so, 1 token every 12 minutes)
            Bandwidth(
                capacity = 5, // 5 uses
                Refill(
                    type = RefillType.GREEDY,       // Gradually
                    tokens = 5,                     // gives 5 tokens
                    period = 1,                     // every 1
                    periodUnit = ChronoUnit.HOURS   // hour
                )
            ),
            // 2 uses, 2 tokens gets added at once after 2 minutes
            // This is to prevent the user from spamming the command
            Bandwidth(
                capacity = 2, // 2 uses
                Refill(
                    type = RefillType.INTERVAL,     // At once,
                    tokens = 2,                     // give 2 tokens
                    period = 2,                     // every 2
                    periodUnit = ChronoUnit.MINUTES // minutes
                )
            ),
        ]
    )
    @JDASlashCommand(name = "rate_limit")
    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        event.reply("Hello world!").await()
    }
}
@Command
public class SlashRateLimit {

    // A rate limit for this slash command only
    @RateLimit(
            // Apply to each user, regardless of channel/guild
            scope = RateLimitScope.USER,
            // Delete the message telling the user about the remaining rate limit after it expires
            deleteOnRefill = true,
            // At least one of those needs to be empty for the command to be rejected
            bandwidths = {
                    // 5 uses, 5 tokens gets added gradually over an hour (so, 1 token every 12 minutes)
                    @Bandwidth(
                            capacity = 5, // 5 uses
                            refill = @Refill(
                                    type = RefillType.GREEDY,       // Gradually
                                    tokens = 5,                     // gives 5 tokens
                                    period = 1,                     // every 1
                                    periodUnit = ChronoUnit.HOURS   // hour
                            )
                    ),
                    // 2 uses, 2 tokens gets added at once after 2 minutes
                    // This is to prevent the user from spamming the command
                    @Bandwidth(
                            capacity = 2, // 2 uses
                            refill = @Refill(
                                    type = RefillType.INTERVAL,     // At once,
                                    tokens = 2,                     // give 2 tokens
                                    period = 2,                     // every 2
                                    periodUnit = ChronoUnit.MINUTES // minutes
                            )
                    ),
            }
    )
    @JDASlashCommand(name = "rate_limit")
    public void onSlashRateLimit(GuildSlashEvent event) {
        event.reply("Hello world!").queue();
    }
}

Cooldown

A cooldown can be used as @Cooldown(5, ChronoUnit.SECONDS /* also scope and deleteOnRefill */).

Using an existing rate limiter

Nothing as simple as using @RateLimitReference with the group of a rate limiter defined by a RateLimitProvider.

We'll be using the same one as in "Registering the rate limiter".

@Command
class SlashRateLimitExisting {

    // A rate limit for this slash command only
    @RateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
    @JDASlashCommand(name = "rate_limit_existing")
    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        event.reply("Hello world!").await()
    }
}
@Command
public class SlashRateLimitExisting {

    // A rate limit for this slash command only
    @RateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
    @JDASlashCommand(name = "rate_limit_existing")
    public void onSlashRateLimit(GuildSlashEvent event) {
        event.reply("Hello world!").queue();
    }
}

Declarative commands

Using an (anonymous) rate limiter

@Command
class SlashRateLimit : GlobalApplicationCommandProvider {

    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        event.reply("Hello world!").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("rate_limit", function = ::onSlashRateLimit) {
            // Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
            val bucketConfiguration = Buckets.spikeProtected(
                capacity = 5,               // 5 uses
                duration = 1.hours,         // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
                spikeCapacity = 2,          // 2 uses
                spikeDuration = 2.minutes   // Give 2 tokens every 2 minutes
            )

            val rateLimiter = RateLimiter.createDefault(
                // Apply to each user, regardless of channel/guild
                RateLimitScope.USER,
                // Delete the message telling the user about the remaining rate limit after it expires
                deleteOnRefill = true,
                // Give our constant bucket configuration
                configurationSupplier = bucketConfiguration.toSupplier()
            )

            rateLimit(rateLimiter)
        }
    }
}

Cooldown

A cooldown can be used as cooldown(5.seconds /* also scope and deleteOnRefill */), instead of the last rateLimit call.

Using an existing rate limiter

Nothing as simple as using rateLimitReference with the group of a rate limiter defined by a RateLimitProvider.

We'll be using the same one as in "Registering the rate limiter".

@Command
class SlashRateLimitExisting : GlobalApplicationCommandProvider {

    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        event.reply("Hello world!").await()
    }

    override fun declareGlobalApplicationCommands(manager: GlobalApplicationCommandManager) {
        manager.slashCommand("rate_limit_existing", function = ::onSlashRateLimit) {
            // Use the rate limiter we defined in [[WikiRateLimitProvider]]
            rateLimitReference(WikiRateLimitProvider.RATE_LIMIT_GROUP)
        }
    }
}

Cancelling rate limits

If your interaction does an early return, you can also return the token with cancelRateLimit(), so the user won't get penalized for this interaction.

Cancelling when not in a voice channel

    suspend fun onSlashRateLimit(event: GuildSlashEvent) {
        // Assuming we have voice states cached
        if (!event.member.voiceState!!.inAudioChannel()) {
            // Note that this would be easier done using a filter,
            // as no token would be used, and would also be cleaner.
            event.cancelRateLimit()
            return event.reply_("You must be in a voice channel").awaitUnit()
        }

        event.reply("Hello world!").await()
    }
    public void onSlashRateLimit(GuildSlashEvent event) {
        // Assuming we have voice states cached
        if (!event.getMember().getVoiceState().inAudioChannel()) {
            // Note that this would be easier done using a filter,
            // as no token would be used, and would also be cleaner.
            event.cancelRateLimit();
            event.reply("You must be in a voice channel").queue();
            return;
        }

        event.reply("Hello world!").queue();
    }