Skip to content

Using rate limits

Rate limits lets you reject interactions when they are used too much in a time span.

For more details, check out the docs of Bucket4J

Defining a rate limit

Rate limits can be defined by implementing RateLimitProvider, the overridden function will run before registering commands, so you can use them anywhere.

Defining bucket configurations

A BucketConfiguration defines what the limits are, you can supply different configurations based on the context, by implementing a BucketConfigurationSupplier, or you can create a configuration with the factories in Buckets, or with the BucketConfiguration builder.

Tip

If you want to give the same configuration regardless of context, you can use BucketConfigurationSupplier.constant(BucketConfiguration) (or BucketConfiguration#toSupplier() for Kotlin users)

Example

// 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
)
// Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
final var bucketConfiguration = Buckets.createSpikeProtected(
        5,                      // 5 uses
        Duration.ofHours(1),    // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
        2,                      // 2 uses
        Duration.ofMinutes(2)   // Give 2 tokens every 2 minutes
);

Creating a rate limiter

The default rate limiters should provide you ready-made implementations for both in-memory and proxied buckets, refer to the example attached to them.

Example

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()
)
final var rateLimiter = RateLimiter.createDefault(
        // Apply to each user, regardless of channel/guild
        RateLimitScope.USER,
        // Give our constant bucket configuration
        BucketConfigurationSupplier.constant(bucketConfiguration),
        // Delete the message telling the user about the remaining rate limit after it expires
        true
);

However, you can also create a custom one by implementing RateLimiter, which is the combination of:

  • BucketAccessor: Retrieves a Bucket based on the context
  • RateLimitHandler: Handles when an interaction has been rate limited (often to tell the user about it)

Tip

When making a custom rate limiter, you can delegate one of the default implementations to avoid reimplementing existing behavior.

You can also use BucketKeySupplier to help you define functions returning a bucket key (an identifier basically), based on the execution context.

Registering the rate limiter

You can now register using RateLimitManager#rateLimit(String, RateLimiter), the group (name of the rate limiter) must be unique.

Full example

@BService
class WikiRateLimitProvider : RateLimitProvider {

    override fun declareRateLimit(manager: RateLimitManager) {
        // 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()
        )

        // Register, any command can use it
        manager.rateLimit(RATE_LIMIT_GROUP, rateLimiter)
    }

    companion object {
        // The name of the rate limit, so you can reference it in your commands/components
        const val RATE_LIMIT_GROUP = "Wiki"
    }
}
@BService
public class WikiRateLimitProvider implements RateLimitProvider {
    // The name of the rate limit, so you can reference it in your commands/components
    public static final String RATE_LIMIT_GROUP = "Wiki";

    @Override
    public void declareRateLimit(@NotNull RateLimitManager rateLimitManager) {
        // Lets the user use the command 5 times in an hour, but also 2 times in 2 minutes to prevent spam
        final var bucketConfiguration = Buckets.createSpikeProtected(
                5,                      // 5 uses
                Duration.ofHours(1),    // Gives 5 tokens gradually, during an hour (1 token every 12 minutes)
                2,                      // 2 uses
                Duration.ofMinutes(2)   // Give 2 tokens every 2 minutes
        );

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

        // Register
        rateLimitManager.rateLimit(RATE_LIMIT_GROUP, rateLimiter);
    }
}

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.

Example

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();
}