Discord Bot Platform

Solution Architecture v1.0 · Microsoft Azure · Integration Platform Architecture · 2026-10 · 19 views · 12 architecture decision records

The half of a Discord-style chat service that third-party bots talk to: a sharded WebSocket Gateway, HTTP interactions with a three-second acknowledgement, a REST API with per-route rate limits, and server-owned installation. Modelled on the public Discord developer documentation; every number is a stated assumption in ask.md.

19 views 19 HTML views19 SVG19 draw.io 2 documents Updated 2026-10-23
Architecture views

19 views, each in three formats.

Open a view to read it in full. Every SVG carries its diagram source inside it, so it opens in diagrams.net fully editable with no import step; the draw.io files are the same diagrams as plain source.

  1. 01
    System Context

    Who the platform talks to, and which side of the trust line each one is on.

  2. 02
    High-Level Architecture

    Left to right: commit, entitle, route, deliver. The event is filtered before it is serialised.

  3. 03
    Actors and Core Journeys

    Builders, communities and operators, in their own words.

  4. 04
    Journey — Developer, First Bot

    The trough is the silent failure: a bot that connects and receives nothing.

  5. 05
    Journey — Member, Runs a Slash Command

    A silent failure is the worst outcome; the platform shows one within 3 seconds.

  6. 06
    Layered Architecture

    Six layers; nothing lower calls upward.

  7. 07
    Containers and Components

    The shard router is arithmetic; the lease cache makes revocation cheap.

  8. 08
    Interface Catalogue

    Four ways in, three ways out.

  9. 09
    Data Architecture and Storage Zones

    Four zones, classed by how they can be lost.

  10. 10
    Data Model

    Installation, not application, is the unit of permission.

  11. 11
    Critical Flow — Slash Command to Reply

    Message 7 is the contract: acknowledge inside 3 seconds or the user is told it failed.

  12. 12
    Event Fan-out Pipeline

    Filter before serialise; shed at the session, never upstream.

  13. 13
    Critical Flow — Reconnect and Resume

    Resume is cheap; identify is throttled.

  14. 14
    Deployment Architecture

    Each region is primary for its own shards and standby for a neighbour's.

  15. 15
    Release and Environments

    Release gates on resume success, not on error rate alone.

  16. 16
    Observability

    Developer-visible health is the same data as operator health.

  17. 17
    Gateway Session Lifecycle

    A ring that closes: every disconnect is a resumable state, not an error.

  18. 18
    Security and Trust Zones

    Every crossing is authenticated; every bot-supplied URL is hostile.

  19. 19
    Identity — Install and Token Use

    The server grants; the bot presents; the platform checks both, repeatedly.

Documents

The written architecture, on the page.

The view index above is the map; this is the argument. The one-pager and the decision record are part of the deliverable, so they are printed here in full — each also opens as its own page with a table of contents.

Document 1 of 2 · 6 min read

Architecture One-Pager

Discord Bot Platform · Solution Architecture v1.0 · Microsoft Azure · Integration Platform Architecture · 2026-10 · 19 views · 12 architecture decision records

Bots are untrusted consumers on the far side of a one-way boundary: entitlement is enforced before an event is produced, the shard is an address you compute, and every promise states what it loses.

A chat service with 150 million users lets anyone register code that reacts to messages. Some bots are slow. Some are offline for days. A few are hostile. One, at any moment, is a viral slash command running on a serverless function with a cold start — and a person is staring at a spinner waiting for it. In the obvious architecture each of these problems leaks back into the chat service: a slow bot slows a message, a reconnecting fleet takes down the registry, and a revoked bot keeps reading for an hour.

The chat service commits and emits an event, and its involvement ends. Fan-out consumes events partitioned by server, filters them by the bot's intents and the server's permissions before anything is serialised, and routes each to a shard that is simply arithmetic on the server identifier. Gateway pods hold the sockets, with a bounded buffer per session and a 120-second resume window; a bot that reads too slowly is closed resumably, not allowed to grow memory. Slash commands arrive by signed HTTPS or the socket and carry a three-second acknowledgement the platform enforces, with deferral to fifteen minutes. REST actions pass a per-route-bucket limiter whose headers tell the client exactly what to do. Installations belong to the server, and revocation pushes to every node with a ten-second lease as the backstop. Reconnect storms are smeared by server-directed jitter and an identify throttle.

What it is, and what it is not

  • Entitlement enforced at the source — not send-everything-and-filter-in-the-client, which leaks data and spends bandwidth.
  • Shard as arithmetic — not a routing table that becomes a hot, replicated dependency.
  • A published loss boundary at 120 seconds — not an exactly-once claim the platform cannot keep.
  • A three-second acknowledgement — not unbounded waiting on a stranger's endpoint.
  • Server-owned installations — not grants tied to developer accounts that vanish.
  • State classed by how it may be lost — not one strongly consistent store for everything.
  • Topological containment of egress — not an SSRF validator as the only defence.

The decisions that are the architecture

  1. The chat service never waits on a bot (ADR-01) — Events leave over a one-way stream; bot actions return through a separate API edge.
  2. Filter before serialising, route by arithmetic (ADR-02) — Nothing unentitled is produced, and shard placement needs no lookup.
  3. Three seconds, then a visible failure (ADR-03) — The wait on a stranger is bounded and the expiry is shown to the user.
  4. A loss boundary that is published (ADR-04) — Ordered replay for 120 s, explicit loss after, bounded buffers throughout.
  5. Partition by server (ADR-05) — A hot server is one partition, not many copies.
  6. The server owns the grant (ADR-06) — Permissions are evaluated on every call against current roles.
  7. Directed, throttled reconnects (ADR-07) — The recovery path is cheaper than the failure path.
  8. Revocation has a deadline (ADR-08) — Push plus a ten-second lease.
  9. Limits that can be read (ADR-09) — Bucketed, with headers, escalating to bans only for offenders.
  10. Egress contained by topology (ADR-11) — A separate zone with a published range.

Why this should still be right in ten years

A bot platform outlives its first generation of bots, because developers write against the protocol and never rewrite it. These are the properties that should survive a change of scale, of cloud and of staff.

  • The trust boundary depends on no technology. ADR-01 survives replacing Event Hubs, AKS and Azure. What would erode it is an exception for a first-party bot that blocks sending a message.
  • The shard formula is public API. Changing it breaks every deployed bot, which is exactly why it is arithmetic and documented, not infrastructure.
  • Explicit loss boundaries age well. Numbers change; the stance that every promise states what it drops does not.
  • Ownership follows consequence. The server bears the risk of a bot, so the server holds the grant. That rule is independent of scale.

Non-functional targets

Every number here is a stated assumption from the requirement, chosen to be argued with. The right-hand column names the view where the mechanism that meets it is drawn.

Quality Target How it is met View
Gateway delivery latency p50 ≤ 120 ms, p99 ≤ 800 ms commit to socket One filter and one shard hop; no registry call per event, because entitlements are leased. 12
Gateway availability ≥ 99.95 % monthly Per-shard session continuity; node loss is a resume, not an outage. 13
Interaction acknowledgement 3 s hard; POST ≤ 300 ms p99 after click Isolated egress, one retry in-window, enforced timer. 11
Event throughput 400,000 events/s, 3× burst for 60 s Server-partitioned log and arithmetic routing scale horizontally. 12
REST throughput 90,000 requests/s, 4× burst Stateless API pods and regional bucket counters. 14
Revocation ≤ 10 s effective Push invalidation with a 10 s lease backstop. 19
Region loss RTO 5 min new sessions; registry RPO 0 Registry replicates; sessions are rebuilt by reconnecting. 14
Reconnect storm 500,000 sessions in 10 min, registry p99 ≤ 50 ms Server-directed jitter and identify throttle. 13

Scope

In scope

  • Gateway sessions, sharding, intents, resume.
  • Interactions with signing, acknowledgement and deferral.
  • REST with route-bucket limits and permission evaluation.
  • Installation, grants, tokens and revocation.
  • Developer-facing health and test tooling.
  • Egress isolation for bot-supplied URLs.

Explicitly out of scope

  • Message storage and the chat service itself.
  • Voice media.
  • Bot hosting and runtimes.
  • A paid marketplace.
  • User-facing clients.
  • Abuse model training.

What a four-week prototype should prove

Three claims are cheap to test and expensive to be wrong about.

  1. Reconnect storm: kill a node holding 40,000 sessions and measure registry p99 and time-to-resume with and without the identify throttle.
  2. Slow consumer: one bot reading at 1 % of event rate, measuring pod memory and the effect on neighbouring sessions at the buffer bound.
  3. Revocation: revoke a grant under a registry brownout and measure the worst-case time until the session stops receiving events.
  • A gateway node holding 40,000 sessions is killed during peak: every session resumes or re-identifies inside 10 minutes and registry p99 stays under 50 ms.
  • One bot stops reading while its server posts 5,000 messages per second: its session closes resumably at the buffer bound, and no neighbouring session on the pod sees added latency.
  • An administrator revokes a bot during a registry brownout: the bot stops receiving events inside 10 seconds, or the scenario records exactly how long it took.

Open risks, carried rather than hidden

Risk If it lands Response
The 120-second window is too short or too long Too short and ordinary flaky networks force identifies; too long and memory outgrows budget. Instrument reconnect-time distribution before fixing the number; revisit quarterly.
A first-party exception erodes the trust boundary A bot that must block a message puts a stranger on the write path. Make such a feature a separate, explicitly synchronous product with its own SLO.
Serverless bots miss 3 seconds at scale A viral command shows failures the platform is blamed for. Deferral guidance, keep-warm recommendations and per-application ack-rate visibility in the portal.

The reasoning behind every component and technology choice is in the Architecture Decision Record: 12 records across 11 areas, each with the alternatives that lost and what the choice costs.

Document 2 of 2 · 24 min read

Architecture Decision Record

Discord Bot Platform · Solution Architecture v1.0 · Microsoft Azure · Integration Platform Architecture · 2026-10 · 19 views · 12 architecture decision records

The argument these decisions serve is summarised in the Architecture One-Pager.

Twelve decisions make up this architecture. Everything else across the nineteen views is convention, sizing or consequence. Each record states the forcing question, the context that makes it hard, what was decided, how it is realised on Microsoft Azure, the options weighed, what the choice buys and costs, the conditions that would flip it, why it should still be right in ten years, and the lesson worth carrying to a different system.

Status of this document. This is a design, not a report on a running system. It is modelled on the public Discord developer documentation, and every rate, latency, volume, retention and window is the requirement's stated assumption for a large consumer chat service — 150 M monthly users, 2.5 M bots online, 400,000 events per second delivered — invented to be defensible and arguable. They should be replaced with measured telemetry before build, and three in particular are the first numbers real data should overturn: the 120-second resume window, which decides per-session memory; the 10-second revocation lease, which decides registry load; and the 3-second acknowledgement, which the interaction product depends on.

How to read a record

  • Question: The forcing question: why a decision was needed at all.
  • Context: The requirement, the scale and the constraint that make it hard.
  • Decision: What this architecture does, stated so it can be checked.
  • How it is realised on Microsoft Azure: The concrete mechanism: which service or package, configured how, in which project.
  • Options weighed: Chosen, rejected, deferred, or right elsewhere, with the reason for each.
  • Consequences: What the choice buys and what it costs, both kept visible.
  • Choose differently when: The conditions that would flip the decision for your system.
  • Why it holds up over time: What keeps the decision right as scale, staff and technology change.
  • Lesson: The principle that transfers beyond this platform.

Decision map

Trust boundary: Where the chat service stops depending on anything a bot does.

  • ADR-01 · The chat service never waits on a bot

Fan-out: How an event finds the bots entitled to it, and how it is placed.

  • ADR-02 · Entitlement is enforced before serialisation; the shard is a computed address
  • ADR-05 · Partition by server, not by bot

Interactions: The three-second window and what happens when it expires.

  • ADR-03 · A published three-second acknowledgement, with deferral

Delivery contract: What a bot may rely on, stated with its loss boundary.

  • ADR-04 · At-most-once beyond 120 seconds, ordered at-least-once inside it, with bounded buffers

Authority: Who owns a grant, and when it is evaluated.

  • ADR-06 · The server owns the installation, evaluated at request time

Resilience: How a fleet recovers without becoming the outage.

  • ADR-07 · Reconnects are directed, jittered and throttled

Revocation: How a changed grant bites on an open session.

  • ADR-08 · Revocation propagates by push, bounded by a short lease

API: How limits are expressed to bots.

  • ADR-09 · Rate limits are per route bucket, and tell the client what to do

State: Which state is precious and which is rebuilt.

  • ADR-10 · State is classed by how it can be lost

Egress: Calling stranger-chosen URLs safely.

  • ADR-11 · Everything bot-supplied is hostile, and egress is its own zone

Identity: Credentials, scope and leaks.

  • ADR-12 · Tokens are scoped, hashed and scanned for

Technology by capability

Every capability and what it is realised with, the origin of that choice, the alternative that was weighed, and the record that argues it. The requirement stays vendor-neutral; this table is where the architecture commits.

Capability Choice Origin Credible alternative Why this one Record
Gateway sessions AKS StatefulSet, one pod group per shard range Azure Service Fabric or VMs Sockets are stateful and pinned to a shard; StatefulSet gives stable identity and graceful drain. ADR-02
Event distribution Event Hubs, partitioned by server_id Azure Service Bus topics Ordered, partitioned, high-throughput log with consumer groups; topics lack the ordering and rate profile. ADR-05
Registry Cosmos DB, single-write region, zone redundant Azure Azure SQL Small, read-heavy, point-read dominated, globally replicable with a tunable consistency level. ADR-10
Session and counters Azure Cache for Redis, regional, no cross-region replication Azure In-pod memory only Rebuildable state with sub-millisecond access; cross-region replication would add cost for state that is cheaper to rebuild. ADR-04
Edge Front Door with WAF, L4 load balancer for WebSockets Azure Application Gateway Global entry and TLS offload for HTTPS; long-lived WebSockets need a layer-4 path. ADR-07
Revocation fan-out Event Grid Azure Redis pub/sub Reliable push with retry and dead-lettering to every node; pub/sub loses messages on disconnect. ADR-08
Token storage Key Vault, salted hashes Azure Application-level encryption Managed key custody and audit with no custom crypto. ADR-12
Egress Dedicated AKS pool, NAT with fixed public IPs Azure Azure Firewall only Fixed range for allowlists and topological containment. ADR-11
Evidence Blob storage plus Log Analytics Azure Cosmos DB audit container Append-only retention at low cost; query via Log Analytics. ADR-10

The decisions, and the alternatives that lost

Trust boundary

Where the chat service stops depending on anything a bot does.

ADR-01 · The chat service never waits on a bot

Status: Accepted · Shown on views: 01, 02, 06, 12

Does anything on the chat service's path depend on a bot being up, fast or correct?

Context. A bot is code the platform neither wrote nor hosts. It will be slow, offline, buggy, or malicious, and the platform has 150 M humans using a chat service whose latency does not care about any of that. The obvious integration calls bots inline: on a message, find the installed bots and notify them. Every variant that keeps bots on the path makes a human's message latency a function of a stranger's uptime.

Decision. Bots are consumers on the far side of a one-way trust boundary. The chat service commits and emits an event; everything after that is the bot platform's own time. Bot actions return through a separate API edge and never through the event path. The chat service has no synchronous dependency on any bot-platform component.

How it is realised on Microsoft Azure. Commit events are written to Event Hubs partitions keyed by server. The bot platform consumes them with its own consumer groups and checkpoints; no chat-service call blocks on delivery, and a stalled consumer group lags harmlessly. REST actions enter via a separate API edge that calls the chat service through its normal internal API, subject to its own rate limits.

Option Verdict Reasoning
Event stream consumed asynchronously by the bot platform Chosen The chat service is indifferent to bot-platform health. Costs a hop, covered by the p99 800 ms delivery objective.
Inline notification to installed bots at commit Rejected Couples message latency to bot count and bot health. Fast in a demo, an incident at scale.
Bots poll a REST endpoint for new messages Right elsewhere Right for very small ecosystems or batch bots that tolerate delay; wasteful and slow at 2.5 M online bots.

What it buys

  • A total bot-platform outage degrades bots and nothing else.
  • Chat-service releases and bot-platform releases are decoupled.

What it costs

  • One extra hop before a bot sees an event.
  • The bot platform must own its delivery guarantees, which is why the loss window is published (ADR-04).

Choose differently when. Inline notification would be defensible only for a single internal consumer on the same team, where the write is already slow enough to absorb one more call.

Why it holds up over time. This is the decision least tied to any technology on the page, and the most likely to be eroded by a feature request such as an official bot that blocks sending a message. Hold the boundary and make the exception a separate, explicitly synchronous product.

Lesson. Somebody else's uptime must never appear in your write path.

Fan-out

How an event finds the bots entitled to it, and how it is placed.

ADR-02 · Entitlement is enforced before serialisation; the shard is a computed address

Status: Accepted · Shown on views: 02, 07, 12

Where is an event filtered by what a bot may see, and how does an event find the right session?

Context. A bot receives only what its intents and the server's permissions allow. Filtering after the fact — send everything, let the client drop it — leaks data and burns bandwidth for 400,000 events per second. Routing is the other half: with millions of sessions, a lookup table from server to session is a hot, replicated structure that must itself be reliable.

Decision. Filter by intents and permissions at the source, before the event is serialised for a bot; the unfiltered event is never produced. Shard assignment is a pure function: shard = (server_id >> 22) mod shard_count. Any gateway or fan-out node routes any server to any shard with no lookup.

How it is realised on Microsoft Azure. The entitlement filter runs in fan-out workers on AKS, reading cached installation leases (10 s TTL) from Cosmos DB. The router computes the shard arithmetically and writes to that shard's Event Hubs partition group. Gateway shards own sockets for their shard number only.

Option Verdict Reasoning
Filter at source; shard as arithmetic Chosen Nothing unentitled is produced; no routing table to keep consistent. Costs a filter evaluation per event per bot, mitigated by lease caching.
Send all, filter in the client Rejected Leaks data to untrusted code and multiplies egress.
Server-to-session routing table Right elsewhere Right when sessions must move between nodes often; here the shard count is fixed per bot, so arithmetic is cheaper and has no lookup outage.

What it buys

  • No unentitled byte ever reaches a bot.
  • Routing needs no shared state and survives a registry outage.

What it costs

  • Changing shard count re-maps servers, so re-sharding needs a bounded dual-delivery window.
  • The filter is a hot path and must stay allocation-light.

Choose differently when. A routing table is justified if sessions must live-migrate for load balancing; with fixed shard counts per bot it is not.

Why it holds up over time. Arithmetic placement and filter-at-source are protocol-level and survive any infrastructure change. Changing the formula breaks every deployed bot, so treat it as public API.

Lesson. Compute the address; never look it up.

ADR-05 · Partition by server, not by bot

Status: Accepted · Shown on views: 06, 12

What is the unit of fan-out partitioning?

Context. A server with 2 million members and 40 bots generates events at a rate that varies by four orders of magnitude from the smallest server. If partitioning is by bot, a hot server's cost is multiplied by every bot in it and by every shard those bots run.

Decision. Events are partitioned by server identifier. Each event is produced once into its server's partition, filtered per installed bot, and routed to each bot's shard. Hot servers are isolated to their own partitions and can be given a dedicated class later.

How it is realised on Microsoft Azure. Event Hubs with server_id as partition key and consumer groups per fan-out worker pool. Partitions are provisioned in multiples with headroom; a hot-server partition class (Phase 3) uses dedicated throughput units.

Option Verdict Reasoning
Partition by server Chosen One copy of each event; hot servers isolate naturally. Each bot subscribes via the filter, not via partition.
Partition by bot Rejected A hot server's cost is multiplied by every bot installed there.
Partition by channel Right elsewhere Finer, right if single channels become the hot unit; adds partitions and cross-channel ordering questions.

What it buys

  • Per-event cost does not grow with bots per server.
  • A hot server cannot starve others.

What it costs

  • Partition count must be provisioned ahead of growth.
  • A single server's strict ordering is limited by one partition's throughput.

Choose differently when. By-channel partitioning if channels, not servers, prove to be the hot unit.

Why it holds up over time. Server-keyed partitioning mirrors how humans experience the service. It should hold until servers fundamentally change shape.

Lesson. Partition by the thing whose hotness you need to contain.

Interactions

The three-second window and what happens when it expires.

ADR-03 · A published three-second acknowledgement, with deferral

Status: Accepted · Shown on views: 04, 05, 11

How long does the platform wait on a stranger's endpoint, and what does the user see when it does not answer?

Context. Slash commands are a human clicking, then waiting on code the platform cannot see. Without a bound the user waits indefinitely; with an unbounded retry the platform queues work for dead endpoints. Serverless bots add cold starts that make p99 latency inherently unpredictable.

Decision. Each interaction must be acknowledged within 3 seconds or the platform shows the user a failure and counts the miss. A bot that needs longer sends a deferred acknowledgement immediately and responds within 15 minutes through a one-time token. Delivery is one POST with one retry inside the window.

How it is realised on Microsoft Azure. Interaction Service validates and enqueues; an isolated egress pool sends the signed POST. A timer per interaction fires at 3.0 s, writes an ephemeral failure, and increments the application's miss counter. Interaction tokens are single-interaction scoped and expire at 15 minutes.

Option Verdict Reasoning
3 s ack with deferral to 15 min Chosen Bounded wait for the user; long work still possible. Costs bots a protocol detail they must implement.
Wait until the bot responds Rejected A silent user experience and a queue that grows behind dead endpoints.
Always respond asynchronously Right elsewhere Fits chat-style bots already on sockets; worse UX for simple commands that could answer instantly.

What it buys

  • The user always sees an outcome within 3 seconds.
  • Dead endpoints cost one bounded attempt, not a queue.

What it costs

  • Bots must ack fast; cold-start-prone runtimes need keep-warm or deferral.
  • A viral command converts bot p99 into user-facing failure rate.

Choose differently when. A longer window is right for a closed enterprise where every bot is vetted and hosted; a public ecosystem needs the hard bound.

Why it holds up over time. The number may change, the existence of a bound will not. What would break it is a product request for 'wait up to 30 s' that turns the user's click into an unbounded wait.

Lesson. Bound the wait you cannot control, and turn its expiry into a visible state.

Delivery contract

What a bot may rely on, stated with its loss boundary.

ADR-04 · At-most-once beyond 120 seconds, ordered at-least-once inside it, with bounded buffers

Status: Accepted · Shown on views: 12, 13, 17

What does the Gateway promise a bot that disconnects, and what happens to a bot that reads slowly?

Context. Reliable delivery to 2.5 M bots with arbitrary disconnections would need unbounded per-session buffering. A slow reader would grow memory without bound and hurt every session on its node. Promising exactly-once would be a claim the platform cannot keep.

Decision. The contract is published: within a 120 s resume window, events are replayed in order from the bot's last sequence number; beyond it, the bot must start a fresh session and accept loss. Each session has a bounded send buffer; at the bound the session is closed with a resumable code rather than allowed to grow.

How it is realised on Microsoft Azure. Per-session ring buffers in gateway-pod memory (120 s or 5,000 events), with a replicated summary in Azure Cache for Redis for node-loss recovery. Close codes distinguish resumable from fatal. A buffer at 80 % emits a degraded metric visible in the developer dashboard.

Option Verdict Reasoning
Bounded resume window with explicit loss Chosen Memory is proportional to sessions, not events. Costs bot authors a documented gap.
Unbounded durable per-bot queue Rejected Becomes a database per bot and a cost line nobody budgeted for.
Exactly-once delivery Rejected Not achievable across a network the platform does not control; the claim would only teach bots not to defend themselves.

What it buys

  • Memory is bounded and predictable.
  • The gap is explicit, so bots can reconcile against REST after a long disconnect.

What it costs

  • Long-offline bots miss events.
  • Sizing resume buffers across millions of sessions is a real memory budget.

Choose differently when. Durable queues are right for paid, contractually-bound integrations; a free public ecosystem cannot afford them.

Why it holds up over time. The principle — bound everything a consumer can make grow — outlasts any specific numbers. The 120 s figure should be revisited with real reconnect-time data.

Lesson. A promise with an explicit boundary is worth more than a bigger promise with a hidden one.

Authority

Who owns a grant, and when it is evaluated.

ADR-06 · The server owns the installation, evaluated at request time

Status: Accepted · Shown on views: 03, 10, 19

Who owns a bot's grant, and when are permissions evaluated?

Context. If grants belong to the developer, deleting a developer account silently breaks hundreds of servers. If permissions are baked into the token at install, narrowing one needs a token reissue and some old tokens keep working.

Decision. Installations belong to the server. A bot token identifies an application; what it may do is the installation's permission bitset intersected with the server's current roles and channel overrides, evaluated on every call.

How it is realised on Microsoft Azure. Installation records in Cosmos DB keyed by (server, application). The permission function is a shared library used by the API edge and the Gateway entitlement filter, so REST and events agree. Bot actions on higher-ranked members are refused regardless of the bitset.

Option Verdict Reasoning
Server-owned installation; evaluate per request Chosen Revocation and narrowing are immediate and local to the server. Costs a lookup per call, mitigated by leases.
Permissions encoded in the token Rejected Narrowing needs reissue; stale tokens keep authority.
Developer-owned grants Rejected Developer churn becomes server outages.

What it buys

  • Server owners can narrow or revoke without touching the developer.
  • REST and Gateway cannot disagree about authority.

What it costs

  • A per-request permission evaluation is a hot-path cost.
  • Cache staleness is bounded only by the lease TTL (ADR-08).

Choose differently when. Token-embedded permissions are fine for machine-to-machine integrations with a few long-lived installs.

Why it holds up over time. Ownership by the party whose data is at risk is a durable rule.

Lesson. Authority belongs to whoever bears the consequence.

Resilience

How a fleet recovers without becoming the outage.

ADR-07 · Reconnects are directed, jittered and throttled

Status: Accepted · Shown on views: 13, 15, 17

How is a fleet of 40,000 reconnecting bots prevented from taking down the registry?

Context. A gateway node dies, or a release drains it, and every bot on it reconnects at once. Identify is expensive: it hits the registry, validates the token, builds the session. Left alone, the fleet's own recovery behaviour is the outage.

Decision. The server instructs reconnecting clients with a jittered delay, throttles identifies per bot and per shard bucket, and prefers resume, which is cheap, over identify. Releases drain nodes through the same mechanism.

How it is realised on Microsoft Azure. The Gateway sends a reconnect opcode with jitter; WebSocket Edge enforces max_concurrency per shard bucket and 1,000 identifies per 24 h per bot using Redis counters. Resume reads the session store and an entitlement lease, not the registry.

Option Verdict Reasoning
Server-directed jitter plus identify throttle Chosen Smears the storm and protects the registry. Costs slower recovery for the largest fleets.
Clients choose their own backoff Rejected Clients are untrusted and many ignore advice.
Over-provision the registry for storms Right elsewhere Valid when storms are rare and registry cost is trivial; here the storm is the sizing driver.

What it buys

  • Node loss and releases do not become registry incidents.
  • The same mechanism serves both failure and deployment.

What it costs

  • Recovery time for large fleets is deliberately bounded below.
  • Throttles need tuning against real storms.

Choose differently when. Over-provisioning is simpler if the registry were a cheap, linearly scalable store.

Why it holds up over time. Controlling reconnect behaviour from the server side is a lasting pattern for any persistent-connection fleet.

Lesson. Make the recovery path cheaper than the failure path.

Revocation

How a changed grant bites on an open session.

ADR-08 · Revocation propagates by push, bounded by a short lease

Status: Accepted · Shown on views: 07, 13, 19

How quickly does a revoked or narrowed grant bite on a session that is already open?

Context. Long-lived sockets cache who they are entitled to. If a grant is revoked at second 0 but the session keeps streaming events for an hour, the admin's action is a lie.

Decision. Revocation pushes an invalidation to every gateway node, and every session's entitlements are held as a lease with a 10 s TTL, so the worst case is the lease length even if the push is lost.

How it is realised on Microsoft Azure. Installation Service writes the change and publishes to Event Grid; gateway nodes invalidate the lease cache and close affected sessions. The lease renewal re-reads Cosmos DB; if the registry is unavailable, existing sessions continue for 5 minutes and new identifies are refused.

Option Verdict Reasoning
Push invalidation plus short lease Chosen Prompt in the normal case, bounded in the failure case.
Check the registry per event Rejected Exact and unaffordable at 400k events/s.
Long-lived entitlements, revoke by reconnect Rejected Staleness is unbounded.

What it buys

  • Revocation has a number, 10 seconds.
  • A lost push degrades to lease expiry.

What it costs

  • Steady background load of lease renewals.
  • Registry outage policy (fail closed for new, run for existing) is a judgement call.

Choose differently when. Per-event checks if the event rate were far lower.

Why it holds up over time. A bounded-staleness lease is a durable pattern. The TTL can be tuned.

Lesson. Revocation is not real until it has a deadline.

API

How limits are expressed to bots.

ADR-09 · Rate limits are per route bucket, and tell the client what to do

Status: Accepted · Shown on views: 08, 16

How are API limits expressed so well-behaved bots never need to guess?

Context. A single global limit punishes a bot for one hot route; per-endpoint secret limits make bots brittle. Bots that ignore limits are common and hostile traffic hides among them.

Decision. Limits are per route bucket and per major resource, with a global ceiling. Every response returns bucket id, remaining and reset time; 429 carries retry-after. Persistent violators are escalated to a timed token ban.

How it is realised on Microsoft Azure. API Edge keeps bucket counters in regional Redis, with local admission and periodic reconciliation. Counter loss gives a brief over-allowance, accepted. Invalid-request counts are tracked per token and per source address.

Option Verdict Reasoning
Per-bucket limits with headers Chosen Predictable and self-documenting. Costs counter state per bucket per resource.
Single global limit Rejected Penalises hot routes and hides the cause.
Hidden limits with random 429s Rejected Teaches clients to hammer.

What it buys

  • Well-behaved clients never hit a limit unannounced.
  • Escalation targets only the offender.

What it costs

  • Counters are approximate, so brief overshoot is possible.
  • Bucket naming becomes public API.

Choose differently when. Central exact counters if overshoot has a direct cost, for example metered billing.

Why it holds up over time. Machine-readable limits are a durable convention.

Lesson. A limit that cannot be read is a trap.

State

Which state is precious and which is rebuilt.

ADR-10 · State is classed by how it can be lost

Status: Accepted · Shown on views: 09, 10, 14

Which state is precious, and which is rebuilt?

Context. Treating all state as precious makes every store a database with replication, backup and failover. Treating all as disposable loses installations.

Decision. Three classes: registry (strong, small, RPO 0), session (rebuildable by reconnecting), counters (approximate and regional). Each class has its own store, durability and recovery story.

How it is realised on Microsoft Azure. Cosmos DB with zone redundancy and a replicated secondary for the registry; Azure Cache for Redis with replicas for session and counters, without cross-region replication; append-only evidence to Blob and Log Analytics.

Option Verdict Reasoning
Three classes with distinct stores Chosen Recovery effort is proportional to loss cost.
One strongly consistent store for all Rejected Cost and latency on a path that does not need it.
Everything in memory Rejected Loses installations.

What it buys

  • A Redis failover is a reconnect, not a data loss.
  • Registry sizing is small and predictable.

What it costs

  • Three operational patterns to run.
  • Class boundaries need discipline as features arrive.

Choose differently when. One store is fine for a small ecosystem.

Why it holds up over time. Classing by loss cost survives any technology swap.

Lesson. Decide how each kind of state may be lost before choosing where it lives.

Egress

Calling stranger-chosen URLs safely.

ADR-11 · Everything bot-supplied is hostile, and egress is its own zone

Status: Accepted · Shown on views: 14, 18

How does the platform call URLs strangers chose without becoming a request-forgery engine?

Context. Interaction endpoints, webhooks and embed images are all URLs a stranger picked. A worker that fetches them with internal network access is an SSRF vector aimed at the metadata service.

Decision. Egress runs in an isolated pool with no route into the platform. Addresses are resolved and validated immediately before connect; private ranges and metadata addresses are denied. The egress range is published so bots can allowlist it.

How it is realised on Microsoft Azure. A dedicated AKS node pool in egress-only subnets with NSG rules denying internal ranges, a NAT with a fixed public IP set, and a validating connect library. Redirects are re-validated.

Option Verdict Reasoning
Isolated egress zone plus validation Chosen Containment holds even if validation has a bug.
Validation code only Rejected One bug away from the metadata service.
Customer-hosted relay Right elsewhere Right for regulated tenants who must control egress.

What it buys

  • A bug in validation cannot reach internal systems.
  • Bots can allowlist a stable range.

What it costs

  • A separate fleet and network to run.
  • Changing the range is a breaking change for bots.

Choose differently when. A hosted relay if the platform must never originate traffic.

Why it holds up over time. Network containment ages better than any validation function.

Lesson. Contain by topology, not by cleverness.

Identity

Credentials, scope and leaks.

ADR-12 · Tokens are scoped, hashed and scanned for

Status: Accepted · Shown on views: 18, 19

How are credentials issued, stored and recovered when they leak?

Context. A bot token is long-lived and pasted into public repositories daily. A token with broad scope is a master key.

Decision. Tokens are salted hashes at rest, shown once, scoped to one application, with interaction tokens scoped to one interaction. Public code hosts are scanned for leaked tokens, which are reset within 5 minutes.

How it is realised on Microsoft Azure. Token Service stores hashes and keys in Key Vault; the scanner is fed by code-host partner programmes; reset invalidates the hash and closes sessions through the same path as revocation (ADR-08).

Option Verdict Reasoning
Hashed, scoped tokens with leak scanning Chosen Limits the blast radius and shortens exposure.
Reversible stored tokens Rejected A database leak becomes a fleet compromise.
Short-lived tokens only Right elsewhere Right for machine-to-machine; hostile to hobbyist developers.

What it buys

  • A leak costs minutes, not days.
  • The database holds nothing replayable.

What it costs

  • A developer must save a token at creation.
  • Scanning depends on partner programmes.

Choose differently when. Short-lived tokens if bots were all hosted by a managed runtime.

Why it holds up over time. Hashing and scoping do not age.

Lesson. Assume the credential will leak, and shorten the consequence.

Every package used, in one table

The terms this package uses in a specific way, and what each one is doing in the architecture.

Package What it is What it does here Considered instead
Shard A numbered slice of a bot's servers, computed from server id and shard count. The unit of connection and of routing; no lookup needed. A routing table that must itself be reliable.
Intent A declared category of events a bot wants, some privileged and reviewed. Entitlement is enforced before serialisation. Filtering in the client.
Interaction A user-initiated command, button or modal delivered to a bot with a 3-second acknowledgement. Makes the stranger's latency visible and bounded. Unbounded waiting.
Resume window The 120 seconds in which a reconnecting session replays missed events. Defines the explicit loss boundary. Unbounded per-bot queues.
Installation A server's grant of permissions to an application. The unit of authority and revocation. A developer-owned grant.
The package

Everything as it was delivered.

These files are served exactly as they were produced — the diagram pages keep their own house style because that is the artifact, not a rendering of it.