Skip to content

Sharding and resuming

Three Bosties running in a row

Discord splits a bot's guilds across gateway connections called shards: guild g belongs to shard (g >> 22) % shard_count. A bot in more than 2500 guilds must shard; Discord closes an unsharded connection with code 4011 and start() raises ShardingRequired.

Shards in one process

Bot(shard_count="auto"), the default, asks Discord how many shards to use and runs them all. Nothing else is needed: bostie starts the shards within Discord's identify limits, and every shard sends its events to the same listeners, cache and handlers. event.shard_id says which shard an event came on.

Shards across processes

import asyncio
import logging
import os

import bostie
from bostie.events import ShardDisconnect, ShardReady

log = logging.getLogger("my-bot")
cluster = int(os.environ.get("CLUSTER", "0"))
identify_lock = asyncio.Lock()


async def wait_for_turn(shard_id: int) -> None:
    async with identify_lock:
        await asyncio.sleep(5)


bot = bostie.Bot(
    shard_count=16,
    shard_ids=range(cluster * 4, cluster * 4 + 4),
    before_identify=wait_for_turn,
    session_file=f"sessions-{cluster}.json",
)


@bot.listen
async def shard_ready(event: ShardReady) -> None:
    log.info("shard %d is ready", event.shard_id)


@bot.listen
async def shard_lost(event: ShardDisconnect) -> None:
    action = "resuming" if event.resuming else "starting a new session"
    log.info("shard %d disconnected (%s), %s", event.shard_id, event.code, action)


bot.run()

shard_count=16 fixes the total, and shard_ids picks the shards this process runs; shard_ids needs an explicit shard_count, and its ids must be distinct and within range(shard_count).

Discord allows a limited number of identifies at a time per bot. bostie respects that limit within one process; across processes, before_identify coordinates them. bostie awaits before_identify(shard_id) before each identify, so it can wait for a slot in a lock your processes share. In the example, identify_lock stands in for that shared lock, held for the 5 seconds Discord asks between identifies of one bucket. If the hook raises, bostie logs the error and identifies anyway.

Shard events

Event Fields Sent when
ShardReady shard_id a shard's new session is ready and its guilds have arrived
ShardDisconnect shard_id, code, reason, resuming a connection dropped; with resuming, the shard resumes its session and replays the events it missed, else it starts a new session
ShardStop shard_id, code, reason a shard stopped for good, because close() ran or Discord ended it
ShardBackpressure shard_id, queued_bytes a shard paused reading because the event loop falls behind

Reconnects and resumes need nothing from you. bot.latency is the mean heartbeat round trip over the shards, and bot.gateway.shard(shard_id).latency one shard's.

Resuming after a restart

A restart normally starts a new session on every shard, and the events of the minutes in between are lost. With Bot(session_file="sessions.json"), close() keeps the sessions open and writes them to the file. The next start() reads the file, deletes it, and resumes each saved session that matches the current shard_count and shard ids, so Discord replays the events that happened while the bot was down. A file that cannot be read is logged and ignored, and shards without a saved session identify.

bot.run() closes on Ctrl+C and SIGTERM, so a restart by a process manager or a deploy saves the sessions. A crash does not, and the next start identifies. An interaction replayed more than 5 seconds after it was created is skipped with a warning, because its token can no longer be answered in time.