CloudLine
Troubleshooting

Slow slash commands

p50 above 1000 ms, p95 above 3000 ms. Where the latency comes from and how to fix it without rewriting your bot.

If the Telemetry panel shows Slash p50 above 1 s or p95 above 3 s, your users are noticing it. Discord's hard cap is 3 seconds — if your bot doesn't respond within that window, Discord shows "interaction failed" to the user and your reply is dropped.

This page walks through the four most-common causes, in order of how often they hit.

What CloudLine measures

The Slash p50 / p95 tiles measure end-to-end perceived latency: from the moment Discord creates the interaction object (interaction.createdTimestamp in discord.js, interaction.created_at in discord.py) to the moment your bot first responds — reply, deferReply, or showModal.

p50 is the typical command time. p95 is the slow tail — 5% of commands took longer than this number. A wide gap between them (e.g. p50 = 300 ms but p95 = 4 s) means your average is fine but specific commands or edge cases are slow.

The same measurement applies to Component (button / select menu), Autocomplete, modal submit, and context menu — they have their own tiles for the same reason.

1. You're doing slow work before responding

The cardinal rule of Discord interactions: respond within 3 seconds, or defer.

If your handler runs a slow database query, a 3rd-party API call, or any non-trivial computation before calling reply(), all of that time counts against the 3-second budget.

The fix — deferReply()

deferReply() (or defer() in discord.py) tells Discord "I heard you, I'm working on it." It buys you a 15-minute window to respond with editReply() / interaction.edit_original_response().

discord.js
async function handleSlowCommand(interaction) {
  await interaction.deferReply()         // <2s — Discord shows "thinking..."
  const result = await slowDatabaseQuery() // can take up to 15 minutes
  await interaction.editReply({ content: `Result: ${result}` })
}
discord.py
async def handle_slow_command(interaction):
    await interaction.response.defer()                       # <2s
    result = await slow_database_query()                     # up to 15 minutes
    await interaction.edit_original_response(content=f"Result: {result}")

The CloudLine SDK measures latency at the FIRST response call (whichever of reply/deferReply/showModal fires first), so once you defer early, the slash tile reflects the defer time, not the total command time. The slow tail moves to a different metric — your own app's processing time.

When NOT to defer

Defer adds visible delay ("thinking..." spinner). For interactions that complete in well under 3 seconds, just reply() directly — it's snappier.

2. Database queries on every command

The classic backend latency leak. Every slash command hits the database, and the queries aren't indexed.

Common cases:

  • SELECT … WHERE user_id = ? without an index on user_id. Linear table scan; gets slower as your bot grows.
  • Multiple sequential queries that could be one query with a join.
  • Queries without LIMIT in moderation / leaderboard commands. They get linearly slower as the dataset grows.

The fix

  • Add an index on every column used in a WHERE clause that runs on a hot command path.
  • Batch reads — for "get all the user's settings" type queries, fetch in one round-trip and parse client-side.
  • Add LIMIT to any query that could ever return more than ~100 rows.

After indexing, you don't need to defer anymore for most commands.

3. Synchronous JS / Python work blocks the event loop

A bot that does heavy synchronous work (regex parsing, JSON marshalling of huge objects, CPU-bound calculation) blocks the event loop. While blocked, NO commands respond — not just the one running. The whole bot pauses.

CloudLine surfaces this as event-loop lag in the Telemetry panel:

  • Idle bot: 0–5 ms
  • Brief GC spikes: 20–50 ms
  • Sustained > 100 ms: the loop is busy doing synchronous work between interactions

If event-loop lag correlates with high slash_p95, you have a blocking-work problem.

Common causes

  • Large JSON or string operations on every command. Cache them.
  • CPU-bound loops (image processing, hashing, complex math). Move to a worker thread (Node) or asyncio.to_thread (Python).
  • Synchronous file I/Ofs.readFileSync, open(...).read(). Use async versions.

4. Discord REST rate limits (429)

If your bot is calling Discord's REST API faster than allowed, those calls hang waiting for the bucket to refill. CloudLine surfaces this as discord_rate_limit_hits:

  • Normal: 0.
  • Any non-zero number: your bot is hitting routes faster than Discord allows.
  • Sustained > 5 per minute: something is wrong.

Common causes

  • An unintended retry loop — a 4xx error that you retry, hitting the same rate limit each time.
  • A broadcast to many channels / guilds without delay between sends.
  • A bot registering slash commands on every startup — startup latency goes up, and if you redeploy often you hit registration limits.

The fix

  • Don't retry 4xx — it never succeeds. Only retry on 5xx and network errors.
  • Throttle broadcasts to a few-per-second rate.
  • Register commands once at deploy time via a separate script, not on every bot startup.

How to find the slow command

Telemetry shows the aggregate p50 / p95 across all commands, not which specific command is slow. If you have a hunch about one command, the simplest way to verify:

const t0 = Date.now()
const result = await handleCommand(interaction)
const ms = Date.now() - t0
console.log(`[${interaction.commandName}] ${ms}ms`)

Run for a few minutes, grep the logs for the slowest entries. From there, profile the specific handler.

Defer-by-default for unpredictable work

If you can't tell whether a command will be fast or slow (varies by user, varies by time of day), defer first and edit later. The 200–500 ms "thinking..." spinner is much better than a "interaction failed" error.

export default async function genericHandler(interaction) {
  await interaction.deferReply()
  try {
    const result = await doTheWork()
    await interaction.editReply({ content: result })
  } catch (err) {
    monitor.captureError(err, { context: { command: interaction.commandName } })
    await interaction.editReply({ content: 'Something went wrong.' })
  }
}

The CloudLine SDK also auto-pushes that captureError() to your Error Log, so you'll see the cause without searching logs.