glyde/identify_queue

Discord’s IDENTIFY concurrency limit, as a pure state machine.

max_concurrency from GET /gateway/bot divides shards into buckets by shard_id % max_concurrency, and one shard per bucket may IDENTIFY every five seconds. Separately a bot has a daily budget of session starts, and exhausting that resets the bot token. Two limits, counted apart.

RESUME never comes here: Discord meters IDENTIFY and not RESUME, so a shard holding a session goes straight to the wire.

A grant is a loan. Release gives it back; if nobody does it lapses after hold_ms, so a lost message costs one bucket half a minute, not forever.

Types

pub type Input {
  Request(shard: Int)
  Release(shard: Int)
  Tick
}

Constructors

  • Request(shard: Int)

    This shard is at the point in its handshake where the next frame it writes is IDENTIFY.

  • Release(shard: Int)

    The shard is done with its place, however it went. Cancels a queued request and clears a grant already given.

  • Tick

    Re-evaluate. Idempotent and always safe.

pub type Notice {
  BudgetLow(remaining: Int)
  Waiting(shard: Int, behind: Int)
}

Constructors

  • BudgetLow(remaining: Int)
  • Waiting(shard: Int, behind: Int)

    behind counts the shards ahead of this one in its bucket. Zero means it is next and the bucket is only waiting out its clock.

pub type Output {
  Grant(shard: Int)
  Deny(shard: Int, refresh_at_ms: Int)
  Wake(in_ms: Int)
  Note(notice: Notice)
}

Constructors

  • Grant(shard: Int)

    Write IDENTIFY for this shard now, as the very next frame.

  • Deny(shard: Int, refresh_at_ms: Int)

    Ask GET /gateway/bot again no sooner than refresh_at_ms, the earliest instant it could report a refilled budget, and build a new queue from the answer. The daily session budget is gone; this queue never grants again, so waiting on it buys nothing and the request is dropped, not parked.

  • Wake(in_ms: Int)

    Call step(_, Tick) again in about this long. A latency optimisation, not a liveness requirement.

  • Note(notice: Notice)
pub opaque type Queue

Values

pub fn new(
  max_concurrency max_concurrency: Int,
  remaining remaining: Int,
  reset_at_ms reset_at_ms: Int,
) -> Queue

From GET /gateway/bot’s session_start_limit. Counts off the wire are clamped, not rejected: a state machine may not crash on a peer’s numbers. reset_at_ms is not clamped. It is an instant on the caller’s clock, which nothing here can judge, so the denial that reads it is held to the present instead.

pub fn rate_limit_key(queue: Queue, shard shard: Int) -> Int

Which bucket a shard identifies through. Discord calls it rate_limit_key and a bot split over several processes routes on it.

pub fn remaining(queue: Queue) -> Int

Session starts left, as this gate has counted them. Never rises: a gate that refills itself hands out the thousandth IDENTIFY twice.

pub fn solo() -> Queue

A single-shard bot, still spacing IDENTIFYs five seconds apart. 1000 is Discord’s default daily budget and a placeholder: past 150 000 guilds the real number is larger, so pass it once GET /gateway/bot answers.

pub fn step(
  queue: Queue,
  now_ms now_ms: Int,
  input input: Input,
) -> #(Queue, List(Output))
pub fn wake_after(
  queue: Queue,
  now_ms now_ms: Int,
) -> option.Option(Int)

When the queue would next do something useful, in milliseconds from now_ms. None means no shard is waiting.

Search Document