Skip to content

Command diffing

Every time your bot starts, it needs to make sure the slash commands registered with Discord match what’s defined in your code. The naive approach is to push all commands on every boot — but Discord’s REST API has rate limits, and pushing unchanged commands wastes that budget for no reason.

Warden’s command diffing system solves this by comparing your local command definitions against what’s currently registered with Discord. If nothing has changed, no API call is made. If a single option description changed on one command, only that command is updated. This makes “sync on boot” completely safe for production — even at scale.

When scan() discovers your command classes, the framework builds a complete picture of your slash command tree — names, descriptions, options, permissions, localizations, and everything else that Discord needs. On boot (or when you run warden sync), this local picture is compared against the commands currently registered with Discord via the REST API.

The comparison happens in three stages:

  1. Fetch — retrieve the currently registered commands from Discord

  2. Diff — compare each local command against its registered counterpart

  3. Push — only send commands that are new, changed, or removed

If the diff is empty — meaning your code matches what Discord already has — no PUT request is made at all. You’ll see this in the logs:

[info] Commands are up to date — skipping sync

The diff is granular. It doesn’t just check whether a command exists — it compares every property that Discord cares about:

PropertyExample change that triggers a sync
nameRenaming /warn to /caution
descriptionChanging “Warn a user” to “Issue a warning to a member”
optionsAdding a new option, changing an option’s type, reordering
option descriptionsChanging “User to warn” to “The member to warn”
option requirementsMaking a previously optional field required
choicesAdding a new severity level to a string choice
permissionsChanging defaultMemberPermissions
localizationsAdding or updating name/description translations
subcommandsAdding, removing, or modifying subcommands
subcommand groupsRestructuring the command tree

Changes to your execute() method, middleware, or cooldowns do not trigger a sync — those are runtime concerns that Discord doesn’t know about.

If you’re running a small bot in one server, you might not notice the difference. But as your bot grows, command diffing becomes increasingly important:

Discord’s REST API has strict rate limits. Every unnecessary PUT request to the commands endpoint counts against your budget. With diffing, a bot that restarts ten times during a deploy pipeline makes zero API calls if the commands haven’t changed.

Pushing commands to Discord takes time — especially for bots with many commands across global and guild-scoped registrations. Skipping the sync when nothing changed shaves seconds off your startup time.

In production, your bot may restart for many reasons — deployments, scaling events, crash recovery. With diffing, these restarts are transparent to Discord. No duplicate registrations, no flickering commands, no wasted API calls.

If you’re running multiple bot processes (for sharding or redundancy), only the first one needs to sync. The rest see that commands are already up to date and skip the API call entirely.

While “sync on boot” is the default, you may prefer to sync commands as a separate step — for example, as part of a CI/CD pipeline. The warden sync console command does exactly this:

Terminal window
pnpm warden sync

This boots the framework (without connecting to the gateway), diffs your commands, and pushes any changes. It’s ideal for deploy scripts:

# In your CI/CD pipeline
steps:
- run: pnpm build
- run: pnpm warden sync
- run: pnpm start

You can combine this with start({sync: false}) to ensure commands are only synced once during deploy, never on process start:

new Bot()
.discover(d => d
.interactions('./dist/{commands,buttons,modals,menus}/**/*.js')
.events('./dist/events/**/*.js'))
.intents(i => i.defaults())
.partials(p => p.defaults())
.start({sync: false});

For more details on the sync CLI, see the Sync guide.

When you switch between guild-scoped and global command registration — or when you remove commands from your codebase — stale registrations can linger in Discord. The warden sync:clean command removes them:

Terminal window
pnpm warden sync:clean

This compares what’s registered with Discord against what’s defined in your code, and removes anything that shouldn’t be there. Common scenarios where this is useful:

  • Switching from guild to global — guild-scoped commands remain registered even after you remove .registerTo(). Users see duplicate commands.
  • Removing a command — if you delete a command class, the framework no longer knows about it, so the normal diff won’t remove it from Discord.
  • Renaming a command — the old name stays registered alongside the new one.