djs-commandsv2 docs
Concepts

Cooldowns

Per-user, per-guild, per-command rate limiting backed by your storage adapter.

Cooldowns rate-limit a command for an actor. The dispatcher checks the cooldown gate after validators pass — if the actor is on cooldown, the user sees a "try again in N seconds" reply and the handler is not invoked.

Configuring a cooldown

Attach cooldown to the command. duration is in milliseconds.

defineCommand({
	name: "vote",
	description: "Vote on the current poll",
	cooldown: { type: "perUser", duration: 30_000 },
	run: async ({ reply }) => {
		await reply("voted");
	},
});

Cooldown types

TypeKey shapeWhen to use
perUsercd:<cmd>:user:<userId>One invocation per user globally — e.g. daily reward.
perGuildcd:<cmd>:guild:<guildId>One invocation per guild — e.g. server-wide announcement.
perUserPerGuildcd:<cmd>:user:<userId>:guild:<guildId>One per user per server — the most common choice for moderation.
globalcd:<cmd>:globalOne invocation across all users and guilds — debug/owner commands.

In DMs, guildId falls back to the literal string "dm" so perGuild and perUserPerGuild keys remain stable.

Storage backends

By default, cooldowns are stored in-process in a Map. That works for development and single-process bots, but loses state on restart and doesn't survive horizontal scale. Pass a cacheAdapter to the handler to use a shared store.

import { createCommandHandler } from "@djs-commands/core";
import { redisCacheAdapter } from "@djs-commands/adapter-redis";
import Redis from "ioredis";

const redis = new Redis(process.env.REDIS_URL);

createCommandHandler({
	client,
	commands: [...],
	cacheAdapter: redisCacheAdapter(redis),
});

Writing a custom CacheAdapter

The contract is three methods. expiresAt is an absolute UNIX millisecond timestamp; ttlMs is the same value expressed as a TTL hint for stores that prefer it (Redis PX, Memcached, etc).

interface CacheAdapter {
	get(key: string): Promise<number | null>;
	set(key: string, expiresAt: number, ttlMs: number): Promise<void>;
	delete(key: string): Promise<void>;
}

get should return null when the key is missing or already expired. The engine uses expiresAt to compute remaining time, so backends that auto-expire keys (Redis with PX) and backends that don't (a SQL cooldowns table) both work — the engine cleans up expired entries on read for the latter.

The Redis adapter writes with SET key expiresAt PX ttl so Redis handles expiry server-side and you never accumulate dead keys.

When cooldowns start

The cooldown is started after command.run returns successfully. If run throws, no cooldown is applied — users won't be punished for a failed invocation.

If you need pre-handler cooldowns (e.g. expensive permission lookups should rate-limit even on failure), use a custom validator that records the timestamp itself.

CooldownEngine directly

The engine is exposed for advanced use — you can construct one yourself to gate non-command flows (autocomplete handlers, button clicks, modal submits) using the same shared cache.

import { CooldownEngine } from "@djs-commands/core";

const engine = new CooldownEngine(redisCacheAdapter(redis));

const remaining = await engine.check(myCommand, { userId, guildId });
if (remaining !== null) {
	// on cooldown — `remaining` ms left
}

await engine.start(myCommand, { userId, guildId });

On this page