Cooldowns
Rate-limit commands per user, per guild, per channel, or globally — with per-role and per-user exemptions and overrides. Cooldowns are enforced automatically by command dispatch…
Per-command
Pass a number (milliseconds) or a full config to any command:
import { command } from "spearkit";
export const daily = command({
name: "daily",
description: "Claim your daily reward",
cooldown: 86_400_000, // once per day, per user
run: (ctx) => ctx.reply("Reward claimed!"),
});Client-wide default
A default applies to every command; a command's own cooldown overrides it.
import { SpearClient } from "spearkit";
const client = new SpearClient({ cooldown: { duration: 3000 } });Scope
scope controls what the cooldown is keyed on. Default "user".
command({
name: "announce",
description: "Post an announcement",
cooldown: { duration: 60_000, scope: "guild" }, // one per guild per minute
run: (ctx) => ctx.reply("Announced."),
});| Scope | Keyed on |
|---|---|
user | the invoking user (default) |
guild | the guild |
channel | the channel |
global | everyone shares one bucket |
Exemptions — who waits and who doesn't
exempt lists users and roles that bypass the cooldown entirely.
command({
name: "purge",
description: "Bulk delete",
cooldown: {
duration: 10_000,
exempt: { roles: ["111111111111111111"], users: ["222222222222222222"] },
},
run: (ctx) => ctx.reply("Purged."),
});Per-role / per-user overrides
overrides gives specific roles or users a different duration (milliseconds).
A user override beats role overrides; among matching roles the most lenient
(shortest) duration wins. Use 0 to effectively disable the wait for them.
command({
name: "search",
description: "Search the archive",
cooldown: {
duration: 10_000, // everyone else
overrides: {
roles: { "333333333333333333": 2_000 }, // VIP role: 2s
users: { "444444444444444444": 0 }, // this user: no wait
},
},
run: (ctx) => ctx.reply("Searching…"),
});The message
message customises what blocked users see — a string, or a function of the
remaining milliseconds.
command({
name: "spin",
description: "Spin the wheel",
cooldown: {
duration: 5_000,
message: (ms) => `Hold on — ${Math.ceil(ms / 1000)}s to go.`,
},
run: (ctx) => ctx.reply("🎡"),
});The manager
client.cooldowns is the shared CooldownManager (also used by
prefix commands). Use it directly for custom flows:
const result = await client.cooldowns.consume("vote", 5_000, {
userId: "1",
roleIds: [],
guildId: null,
channelId: null,
});
if (!result.allowed) console.log(`wait ${result.remaining}ms`);consume records the use and returns { allowed: true } or
{ allowed: false, remaining }. peek checks without recording; reset and
clear drop tracked cooldowns. The in-memory backend stays synchronous;
store-backed backends return a Promise — dispatch always awaits.
Shared / shard-safe clocks
By default each process has its own map. Pass cooldownStore so restarts and
shards share one clock:
import { SpearClient, SqliteStore, redisCooldownBackend } from "spearkit";
// Restart-safe (SQLite). Two processes can still race a hit.
const client = new SpearClient({
cooldownStore: new SqliteStore("data/cooldowns.sqlite"),
});
// Shard-safe: Redis SET NX PX, key lives only while cooling down.
const sharded = new SpearClient({
cooldownStore: redisCooldownBackend(redis),
});A KeyValueStore is wrapped automatically. Use redisCooldownBackend when you
need the atomic NX write rather than a timestamp get/set.
Client
SpearClient is a discord.js Client with command, event and component registries — plus interaction routing — wired up for you. You construct it the same way you construct a…
Scaling: one process to many hosts
The same command/component definitions should work in a hobby bot and a large verified app. Scaling changes the runtime around handlers, not spearkit's definition API.