engineering

Scaling a Discord Bot: Shards, Caches, and Workers

Rank.top Team4 min read

Find the bottleneck, split work where it helps, and keep shared state reliable as your bot grows.

Measure before splitting the bot

Slow commands can come from database queries, a busy event loop, a large cache, or an external API. More processes only help if they address the actual limit. Record command latency, memory use, CPU, query time, and queue age during a busy period.

SymptomCheck firstPossible change
Memory grows over timeUnbounded caches and retained objectsCap caches and investigate leaks
Heartbeats are lateSynchronous or CPU-heavy workMove that work to a worker
Commands wait on the databaseQuery plans and connection countAdd an index or reduce queries
Jobs fall behindArrival rate and job durationAdd bounded worker capacity
Gateway workload is too largeDiscord’s shard recommendationDistribute guilds across shards

Separate Gateway sharding from database sharding

A Gateway shard handles a subset of the bot’s servers through its own connection. A database shard holds a subset of stored data. You may need several Gateway shards while still using one database.

Use your library’s shard manager and the shard recommendation from Get Gateway Bot. Plan starts around Discord’s session limits. The Gateway reference explains shard routing and startup constraints.

Keep slow jobs away from event handling

  1. Acknowledge the request

    Validate input and permissions. If an interaction needs more time, defer its response before doing slow work.
  2. Queue a small job

    Include identifiers and the necessary inputs. Avoid placing tokens or entire message histories in the queue.
  3. Process with a worker

    Use timeouts and bounded concurrency. Make repeated delivery safe with a unique job or operation ID.
  4. Save and report the result

    Persist the outcome before notifying the user. Plan a fallback for jobs that outlive the interaction token.

A durable queue can survive a process restart, but jobs may still be delivered more than once. Design purchases, rewards, and scheduled posts so retrying does not repeat the side effect.

Keep caches bounded and replaceable

Cache frequently read settings when it reduces real database load. Give entries a size limit and expiry, and invalidate them when settings change. A cache miss should have a safe path back to durable storage.

Local cache

Fast and simple, but each process has its own copy. Suitable when a little stale data is acceptable or updates can notify every process.

Shared cache

Useful when several workers need the same short-lived state. It adds a network dependency, connection limits, and another failure mode to handle.

Coalesce simultaneous reads for the same missing key so one expiry does not trigger hundreds of identical queries. Add a little variation to expiry times when many entries are created together.

keep this durable

Purchases and balances belong in the database.

A cache can speed up an entitlement check. It should not be the only record that someone paid or that credits were transferred.

Change one boundary at a time

  • Keep database connection totals within the server’s limit as workers increase.
  • Use structured logs with shard, job, and release IDs.
  • Test a worker crash, a cache outage, and duplicate job delivery.
  • Make migrations compatible with both versions during a rollout.
  • Write a rollback plan before moving data between databases.

Database sharding adds routing, migration, and cross-shard query work. Consider indexes, smaller queries, and archiving before taking it on. See the database guide for the starting choices.

keep reading

More from the blog

All articles