Discord Bot Platform

Architecture Views

19 views, in reading order. Every view ships three ways: an HTML page, an SVG that re-opens in diagrams.net fully editable, and draw.io source.

The half of a Discord-style chat service that third-party bots talk to: how a message in one of 21 billion server memberships reaches only the bots entitled to see it, how a slash command gets an answer inside a three-second window from code the platform does not control, and why one slow, buggy or hostile bot can never slow down a human. Read the set in order — the argument runs from the trust boundary, through the people on each side of it, into the shard that makes the boundary cheap.

Context and scope

What sits inside the boundary, and what is on the untrusted side of it.

People and journeys

The developer who builds, and the member who waits three seconds.

Structure

Planes, components and the interfaces the platform commits to.

Data

What must never be lost, what is rebuilt by reconnecting, and what is approximate.

Runtime

A command, an event, and a reconnect — the three flows that carry the contract.

Operations

Deployment, drain-aware release, observability and the session lifecycle.

Architecture One-Pager

The problem, the shape of the answer, the decisions that carry it, and why it should still be right in ten years.

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 sourcesend-everything-and-filter-in-the-client, which leaks data and spends bandwidth.
Shard as arithmetica routing table that becomes a hot, replicated dependency.
A published loss boundary at 120 secondsan exactly-once claim the platform cannot keep.
A three-second acknowledgementunbounded waiting on a stranger's endpoint.
Server-owned installationsgrants tied to developer accounts that vanish.
State classed by how it may be lostone strongly consistent store for everything.
Topological containment of egressan SSRF validator as the only defence.

The decisions that are the architecture

01The chat service never waits on a bot

Events leave over a one-way stream; bot actions return through a separate API edge.

ADR-01

02Filter before serialising, route by arithmetic

Nothing unentitled is produced, and shard placement needs no lookup.

ADR-02

03Three seconds, then a visible failure

The wait on a stranger is bounded and the expiry is shown to the user.

ADR-03

04A loss boundary that is published

Ordered replay for 120 s, explicit loss after, bounded buffers throughout.

ADR-04

05Partition by server

A hot server is one partition, not many copies.

ADR-05

06The server owns the grant

Permissions are evaluated on every call against current roles.

ADR-06

07Directed, throttled reconnects

The recovery path is cheaper than the failure path.

ADR-07

08Revocation has a deadline

Push plus a ten-second lease.

ADR-08

09Limits that can be read

Bucketed, with headers, escalating to bans only for offenders.

ADR-09

10Egress contained by topology

A separate zone with a published range.

ADR-11

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.

QualityTargetHow it is metView
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

RiskIf it landsResponse
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.

Architecture Decision Record

Why every component and every technology on these 19 views is what it is, and what each choice costs.

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

QuestionThe forcing question: why a decision was needed at all.
ContextThe requirement, the scale and the constraint that make it hard.
DecisionWhat this architecture does, stated so it can be checked.
How it is realised on Microsoft AzureThe concrete mechanism: which service or package, configured how, in which project.
Options weighedChosen, rejected, deferred, or right elsewhere, with the reason for each.
ConsequencesWhat the choice buys and what it costs, both kept visible.
Choose differently whenThe conditions that would flip the decision for your system.
Why it holds up over timeWhat keeps the decision right as scale, staff and technology change.
LessonThe principle that transfers beyond this platform.

Decision map

Trust boundary 1

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

ADR-01The chat service never waits on a bot

Fan-out 2

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

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

Interactions 1

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

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

Delivery contract 1

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

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

Authority 1

Who owns a grant, and when it is evaluated.

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

Resilience 1

How a fleet recovers without becoming the outage.

ADR-07Reconnects are directed, jittered and throttled

Revocation 1

How a changed grant bites on an open session.

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

API 1

How limits are expressed to bots.

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

State 1

Which state is precious and which is rebuilt.

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

Egress 1

Calling stranger-chosen URLs safely.

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

Identity 1

Credentials, scope and leaks.

ADR-12Tokens 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.

Open source This design
CapabilityChoiceOriginCredible alternativeWhy this oneRecord
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 boundaryWhere the chat service stops depending on anything a bot does.

ADR-01

The chat service never waits on a bot

Accepted

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.
Options weighed
  • ChosenEvent stream consumed asynchronously by the bot platform: The chat service is indifferent to bot-platform health. Costs a hop, covered by the p99 800 ms delivery objective.
  • RejectedInline notification to installed bots at commit: Couples message latency to bot count and bot health. Fast in a demo, an incident at scale.
  • Right elsewhereBots poll a REST endpoint for new messages: Right for very small ecosystems or batch bots that tolerate delay; wasteful and slow at 2.5 M online bots.
Consequences
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.
LessonSomebody else's uptime must never appear in your write path.
Shown on views01 02 06 12

Fan-outHow 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

Accepted

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.
Options weighed
  • ChosenFilter at source; shard as arithmetic: Nothing unentitled is produced; no routing table to keep consistent. Costs a filter evaluation per event per bot, mitigated by lease caching.
  • RejectedSend all, filter in the client: Leaks data to untrusted code and multiplies egress.
  • Right elsewhereServer-to-session routing table: 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.
Consequences
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.
LessonCompute the address; never look it up.
Shown on views02 07 12
ADR-05

Partition by server, not by bot

Accepted

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.
Options weighed
  • ChosenPartition by server: One copy of each event; hot servers isolate naturally. Each bot subscribes via the filter, not via partition.
  • RejectedPartition by bot: A hot server's cost is multiplied by every bot installed there.
  • Right elsewherePartition by channel: Finer, right if single channels become the hot unit; adds partitions and cross-channel ordering questions.
Consequences
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.
LessonPartition by the thing whose hotness you need to contain.
Shown on views06 12

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

ADR-03

A published three-second acknowledgement, with deferral

Accepted

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.
Options weighed
  • Chosen3 s ack with deferral to 15 min: Bounded wait for the user; long work still possible. Costs bots a protocol detail they must implement.
  • RejectedWait until the bot responds: A silent user experience and a queue that grows behind dead endpoints.
  • Right elsewhereAlways respond asynchronously: Fits chat-style bots already on sockets; worse UX for simple commands that could answer instantly.
Consequences
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.
LessonBound the wait you cannot control, and turn its expiry into a visible state.
Shown on views04 05 11

Delivery contractWhat 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

Accepted

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.
Options weighed
  • ChosenBounded resume window with explicit loss: Memory is proportional to sessions, not events. Costs bot authors a documented gap.
  • RejectedUnbounded durable per-bot queue: Becomes a database per bot and a cost line nobody budgeted for.
  • RejectedExactly-once delivery: Not achievable across a network the platform does not control; the claim would only teach bots not to defend themselves.
Consequences
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.
LessonA promise with an explicit boundary is worth more than a bigger promise with a hidden one.
Shown on views12 13 17

AuthorityWho owns a grant, and when it is evaluated.

ADR-06

The server owns the installation, evaluated at request time

Accepted

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.
Options weighed
  • ChosenServer-owned installation; evaluate per request: Revocation and narrowing are immediate and local to the server. Costs a lookup per call, mitigated by leases.
  • RejectedPermissions encoded in the token: Narrowing needs reissue; stale tokens keep authority.
  • RejectedDeveloper-owned grants: Developer churn becomes server outages.
Consequences
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.
LessonAuthority belongs to whoever bears the consequence.
Shown on views03 10 19

ResilienceHow a fleet recovers without becoming the outage.

ADR-07

Reconnects are directed, jittered and throttled

Accepted

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.
Options weighed
  • ChosenServer-directed jitter plus identify throttle: Smears the storm and protects the registry. Costs slower recovery for the largest fleets.
  • RejectedClients choose their own backoff: Clients are untrusted and many ignore advice.
  • Right elsewhereOver-provision the registry for storms: Valid when storms are rare and registry cost is trivial; here the storm is the sizing driver.
Consequences
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.
LessonMake the recovery path cheaper than the failure path.
Shown on views13 15 17

RevocationHow a changed grant bites on an open session.

ADR-08

Revocation propagates by push, bounded by a short lease

Accepted

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.
Options weighed
  • ChosenPush invalidation plus short lease: Prompt in the normal case, bounded in the failure case.
  • RejectedCheck the registry per event: Exact and unaffordable at 400k events/s.
  • RejectedLong-lived entitlements, revoke by reconnect: Staleness is unbounded.
Consequences
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.
LessonRevocation is not real until it has a deadline.
Shown on views07 13 19

APIHow limits are expressed to bots.

ADR-09

Rate limits are per route bucket, and tell the client what to do

Accepted

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.
Options weighed
  • ChosenPer-bucket limits with headers: Predictable and self-documenting. Costs counter state per bucket per resource.
  • RejectedSingle global limit: Penalises hot routes and hides the cause.
  • RejectedHidden limits with random 429s: Teaches clients to hammer.
Consequences
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.
LessonA limit that cannot be read is a trap.
Shown on views08 16

StateWhich state is precious and which is rebuilt.

ADR-10

State is classed by how it can be lost

Accepted

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.
Options weighed
  • ChosenThree classes with distinct stores: Recovery effort is proportional to loss cost.
  • RejectedOne strongly consistent store for all: Cost and latency on a path that does not need it.
  • RejectedEverything in memory: Loses installations.
Consequences
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.
LessonDecide how each kind of state may be lost before choosing where it lives.
Shown on views09 10 14

EgressCalling stranger-chosen URLs safely.

ADR-11

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

Accepted

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.
Options weighed
  • ChosenIsolated egress zone plus validation: Containment holds even if validation has a bug.
  • RejectedValidation code only: One bug away from the metadata service.
  • Right elsewhereCustomer-hosted relay: Right for regulated tenants who must control egress.
Consequences
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.
LessonContain by topology, not by cleverness.
Shown on views14 18

IdentityCredentials, scope and leaks.

ADR-12

Tokens are scoped, hashed and scanned for

Accepted

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).
Options weighed
  • ChosenHashed, scoped tokens with leak scanning: Limits the blast radius and shortens exposure.
  • RejectedReversible stored tokens: A database leak becomes a fleet compromise.
  • Right elsewhereShort-lived tokens only: Right for machine-to-machine; hostile to hobbyist developers.
Consequences
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.
LessonAssume the credential will leak, and shorten the consequence.
Shown on views18 19

Every package used, in one table

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

PackageWhat it isWhat it does hereConsidered 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.
Open svg/<view>.svg or drawio/<view>.drawio in draw.io Desktop or at app.diagrams.net to edit. The SVG carries the diagram inside it, so it is both the picture and the source. This folder is self-contained — copy it whole and every link still resolves.