Skip to content

Cooldowns

When your bot is live in a busy server, certain actions shouldn’t be spammed. You don’t want a user firing off /warn ten times in a row, and you definitely don’t want someone repeatedly clicking a “Report User” button to flood your moderator channel. Cooldowns give you a clean, declarative way to rate-limit any interaction.

The @cooldown() decorator works on commands, buttons, modals, menus, and context menus — essentially anything with an execute() method. Place it above your class, and Warden takes care of the rest. If a user (or channel, or guild) is still within the cooldown window, the framework responds with a friendly message and your execute() method is never called.

At its simplest, @cooldown() only needs a duration:

import {command, cooldown, Command, CommandInteraction, Params} from '@warden/core';
@command({name: 'warn', description: 'Warn a member'})
@cooldown({duration: 10})
export default class WarnCommand implements Command {
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
// This won't run if the user already used /warn in the last 10 seconds
}
}

That’s it. One line, and your command is rate-limited. No timers to manage, no state to track, no cleanup to worry about.

The duration option is specified in seconds. You may use any positive number:

@cooldown({duration: 5}) // 5 seconds
@cooldown({duration: 60}) // 1 minute
@cooldown({duration: 300}) // 5 minutes
@cooldown({duration: 3600}) // 1 hour

By default, cooldowns are scoped to the user — meaning each person has their own independent timer. But you may want different behavior depending on the situation. The scope option lets you control this:

ScopeDescription
'user'Each user has their own cooldown timer (default)
'channel'The cooldown applies to everyone in the channel
'guild'The cooldown applies server-wide
'global'The cooldown applies across all servers the bot is in
@command({name: 'lockdown', description: 'Lock down the current channel'})
@cooldown({duration: 60, scope: 'channel'})
export default class LockdownCommand implements Command {
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
// Only one lockdown per channel per minute, regardless of who runs it
}
}

A guild-scoped cooldown is useful for heavy operations that affect the entire server:

@command({name: 'purge-inactive', description: 'Remove inactive members'})
@cooldown({duration: 3600, scope: 'guild'})
export default class PurgeInactiveCommand implements Command {
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
// This can only run once per hour across the entire server
}
}

When a cooldown is active, Warden responds with a default message letting the user know they need to wait. You can customize this with the message option. The {remaining} placeholder is replaced with the number of seconds left:

@command({name: 'warn', description: 'Warn a member'})
@cooldown({
duration: 30,
message: 'Easy there — you can issue another warning in {remaining}s.',
})
export default class WarnCommand implements Command {
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
// ...
}
}

This is especially nice for user-facing interactions where you want the tone to match your bot’s personality:

@button({customId: 'report-user/:userId'})
@cooldown({
duration: 60,
message: 'Your report has been received. You can submit another in {remaining} seconds.',
})
export default class ReportUserButton implements Button {
public async execute(interaction: ButtonInteraction, params: Params): Promise<void> {
// ...
}
}

Out of the box, Warden stores cooldown state in memory. This is perfectly fine for most bots — it’s fast, requires zero configuration, and handles everything automatically.

However, in-memory cooldowns don’t survive bot restarts, and they won’t be shared across multiple instances if you’re running in a clustered setup. If that matters to you, there’s a seamless upgrade path.

When you install @warden/cache, Warden automatically switches to Redis-backed storage for cooldowns. You don’t need to change any of your @cooldown() decorators — the framework detects the cache plugin and uses it transparently:

Terminal window
pnpm add @warden/cache

That’s literally all you need to do. Your existing cooldown decorators will now persist across restarts and work across multiple instances.

The automatic detection is convenient, but sometimes you want explicit control. Maybe you installed @warden/cache for CacheService.remember() but don’t want cooldowns to persist across restarts. You can override the behavior in your config:

config/warden.ts
export default {
cooldowns: {
storage: 'memory',
},
};
ValueBehavior
'auto'Use Redis if @warden/cache is installed, otherwise in-memory (default)
'memory'Always in-memory, even if @warden/cache is available
'cache'Always use the cache plugin — throws at boot if not installed

The 'cache' option is useful when you want a hard guarantee that cooldowns are persistent. If someone accidentally removes the cache plugin, the bot will fail loudly at startup rather than silently falling back to in-memory storage.

Moderators are human, and double-clicks happen. A short cooldown on /warn prevents accidental duplicate warnings:

import {command, cooldown, permissions, Command, Options, CommandInteraction, Params, reply, Embed} from '@warden/core';
@command({name: 'warn', description: 'Issue a warning to a member'})
@cooldown({duration: 10, message: 'Please wait {remaining}s before issuing another warning.'})
@permissions({user: ['ModerateMembers']})
export default class WarnCommand implements Command {
public options = [
Options.user('user', 'User to warn').required(),
Options.string('reason', 'Reason').required(),
];
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
const target = interaction.options.getUser('user')!;
const reason = interaction.options.getString('reason')!;
// ... save warning to database ...
await reply(interaction, Embed.success(`${target.username} has been warned for: ${reason}`));
}
}

If your bot has a “Report” button on messages, you’ll want to stop users from clicking it repeatedly:

import {button, cooldown, Button, Params, reply, Embed} from '@warden/core';
import {ButtonInteraction} from 'discord.js';
@button({customId: 'report-message/:messageId'})
@cooldown({
duration: 120,
scope: 'user',
message: 'You already submitted a report. You can report again in {remaining}s.',
})
export default class ReportMessageButton implements Button {
public async execute(interaction: ButtonInteraction, params: Params): Promise<void> {
const messageId = params.get('messageId')!;
// ... forward the report to the mod channel ...
await reply(interaction, Embed.info('Your report has been sent to the moderator team. Thank you!'));
}
}

Channel-wide slowmode for a ticket command

Section titled “Channel-wide slowmode for a ticket command”

If your bot lets users open support tickets, you may want to prevent a channel from being overwhelmed:

import {command, cooldown, Command, CommandInteraction, Params, reply, Embed} from '@warden/core';
@command({name: 'ticket', description: 'Open a support ticket'})
@cooldown({
duration: 300,
scope: 'channel',
message: 'A ticket was recently opened in this channel. Please wait {remaining}s.',
})
export default class TicketCommand implements Command {
public async execute(interaction: CommandInteraction, params: Params): Promise<void> {
// ... create a new ticket channel ...
await reply(interaction, Embed.success('Your ticket has been created. A moderator will be with you shortly.'));
}
}