Fanout contracts  / field guide
Practitioner field guide · 22 September 2026

Making the client carry it: ten years of Discord's gateway contract

Discord has never published an architecture diagram of the tier that pushes events to third-party apps, but it has published every change to the contract that tier enforces: 237 dated entries between July 2017 and September 2026, 27 of them breaking. Read in order, they show one move repeated for nine years, which is to make the consumer declare, partition, hydrate and meter itself. This guide reconstructs that move, the failures that provoked each step, and the condition under which you can make it yourself.

28 primary sources 3 messaging networks compared 4 published failures Evidence through September 2026 Read: 20 min
01

The territory

A push protocol costs you consumers multiplied by events, and the consumers are somebody else's code, already deployed. Everything you have left is the contract.

237
dated change-log entries, 19 Jul 2017 to 18 Sep 2026
27
of them tagged Breaking Change
70 µs
cost of one process-to-process send inside the realtime tier
2,500
guilds per gateway connection, a hard ceiling on one consumer

State the problem without naming the product. A service holds mutable state that many parties want to watch. It offers a long-lived connection and pushes changes down it. The bill for that arrangement is the number of connected consumers multiplied by the events each one is entitled to, and it grows fastest exactly when the product is succeeding. The service can buy more machines, but it cannot deploy a fix to the consumers, because the consumers are other people's programs running on other people's infrastructure against versions of the protocol some of them pinned years ago. Anything the operator wants to change about consumer behaviour has to travel through the contract, which means an announcement, a deadline, and somebody else's breakage.

Discord is a useful subject for this problem because it has run it in public for a decade and kept the receipts in a repository. The developer documentation lives in discord/discord-api-docs, and inside it developers/change-log.mdx holds 237 dated entries stretching from 19 July 2017 to 18 September 2026, each tagged by surface. Twenty-seven carry the tag Breaking Change. The file is countable, which is the property that makes it worth more than a retrospective blog post: it records what was announced, when, and in what order, without the benefit of hindsight.

The finding that surprised me

The largest capability Discord ever took away, message content, became privileged on 1 September 2022. The pull-shaped replacement for it, interactions and slash commands, shipped on 15 December 2020. The substitute arrived twenty months before the withdrawal, and the same ordering holds for the member list: Get Guild Member and Search Guild Members were the documented alternatives before bulk member requests were rate limited in August 2025. On this evidence the deprecation date is not chosen by the capacity problem. It is chosen by the ship date of the substitute, plus the time it takes the ecosystem to adopt it.

The second thing the record shows is that the contract is load-bearing for two different arguments at once. Discord's own documentation for the bulk member request says the limits exist "due to our privacy and infrastructural concerns with this feature", and that sentence explains why these changes survive review inside a company. A change that only saves the platform money is a negotiation with the developer community. A change that also removes a data-protection liability is a decision. Every large narrowing step in this record, intents in 2020, message content in 2022, the user-count-based review in June 2026, and channel obfuscation in August 2026, can be read both ways, and the announcements lean on the privacy reading.

What this guide covers. The public contract between a fanout platform and third-party consumers: admission, declared subscriptions, delegated partitioning, state hydration, reconnect steering and metering. It uses Discord as the long case, with Matrix and Bluesky as comparison points where their specifications solve the same problem differently. What it deliberately does not cover. Discord's storage tier, its voice infrastructure, its client applications and its internal service topology, none of which appear in the repository record. It also does not cover Discord's own incident write-ups: those are published on discord.com, which the research environment could not reach. Where an incident is cited here it comes from a dated change-log entry or an issue thread, and that limit is a real one.

Figure 1 · Three places to cut the cost of a firehose

State changes
at the source

1 · Server tier
more machines,
cheaper fanout primitives

2 · The contract
declare, shard,
hydrate, meter

3 · A separate
projection service

Discord: Manifold,
ZenMonitor, Rust NIFs

Discord: intents, shards,
rate limits
Matrix: lazy_load_members

Bluesky: Jetstream,
filtered JSON firehose

State changes
at the source

1 · Server tier
more machines,
cheaper fanout primitives

2 · The contract
declare, shard,
hydrate, meter

3 · A separate
projection service

Discord: Manifold,
ZenMonitor, Rust NIFs

Discord: intents, shards,
rate limits
Matrix: lazy_load_members

Bluesky: Jetstream,
filtered JSON firehose

Three networks put the narrowing in three different places, and only the middle one can be enforced against consumers you do not control. Reconstructed from Discord's gateway documentation, the Matrix Client-Server API specification and Bluesky's Jetstream design document.
Diagram source
02

How it is actually built

Six controls, in the order they act on a connection. Every one of them is a place where work moves from the platform to the consumer.

Reading the gateway documentation as an architecture rather than as a manual, the same six controls appear in sequence on every connection, and each one was added separately over the decade. They are worth naming as a set because no single document names them: Discord calls them sharding, intents, chunking, resume and rate limits, Matrix calls its equivalents filters and lazy loading, and Bluesky implements the same effect as a separate service. I am calling the set the narrowing plane. It is the part of a push platform that decides how much of the firehose any given consumer is entitled to, and it is almost entirely enforced at the edge of somebody else's software.

Figure 2 · The narrowing plane, in connection order

1 · Admission
buckets of max_concurrency, 1000 IDENTIFY / 24h

2 · Delegated partitioning
shard_id = (guild_id >> 22) % num_shards, 2500 max

3 · Declared subscription
intents bitfield, privileged subset behind review

4 · Hydration
large_threshold 50-250, chunks of 1000

5 · Reconnect steering
resume_gateway_url, server may evict at any time

6 · Metering
120 events / 60s, op 8 once per guild per 30s

1 · Admission
buckets of max_concurrency, 1000 IDENTIFY / 24h

2 · Delegated partitioning
shard_id = (guild_id >> 22) % num_shards, 2500 max

3 · Declared subscription
intents bitfield, privileged subset behind review

4 · Hydration
large_threshold 50-250, chunks of 1000

5 · Reconnect steering
resume_gateway_url, server may evict at any time

6 · Metering
120 events / 60s, op 8 once per guild per 30s

Each control is a published parameter the consumer must satisfy before it receives anything, and each was added years apart. Reconstructed from gateway.mdx and gateway-events.mdx, checked 2026-09-22.
Diagram source

Admission comes first, and it is a queueing system disguised as a rate limit. An app identifies once per shard, and the documentation states that shards are grouped into buckets by rate_limit_key = shard_id % max_concurrency and that "you must start them by 'bucket' in order". The consumer therefore implements the platform's admission control in its own start-up code. Behind it sits a budget rather than a throttle: 1,000 identify calls per 24 hours, and the documented penalty for exceeding it is not a slow-down but "all active sessions for the app will be terminated, the bot token will be reset, and the owner will receive an email notification". A rate limiter that revokes your credential is a statement about how expensive the protected operation is.

Partitioning is delegated by publishing the formula. Discord gives consumers shard_id = (guild_id >> 22) % num_shards and a hard ceiling of 2,500 guilds per connection, and is explicit that sharding "requires no state-sharing between separate connections to operate" and that num_shards "is only used for routing traffic". That single design property, routing derived from an immutable id rather than from server-side assignment, is what lets the platform avoid holding per-consumer placement state. It is also what lets a consumer run two differently sized fleets in parallel and cut over, which the documentation offers as the supported zero-downtime upgrade path. The same formula reached the HTTP API on 15 September 2026, when Get Current User Guilds began returning 400 Bad Request to large-bot-sharded apps that omit the shard parameter.

The declared subscription is the centre of the whole design. Intents arrived on 14 February 2020 as an optional bitfield sent at handshake time, pitched to developers as a saving for them: "Go on, save yourself some bandwidth and CPU usage." Seven months later, API v8 made them mandatory and removed the older guild_subscriptions flag, and six weeks after that the restrictions were back-applied to v6 so that staying on the old version stopped being an escape route. The mechanism matters more than the syntax: because the declaration arrives in the handshake, before any event has been dispatched, the platform can decide what to route to a connection instead of filtering after the fact. A filter applied after fanout saves bandwidth. A filter applied before fanout saves the fanout.

Hydration is the oldest control and the one that kept being tightened. The identify payload has always carried large_threshold, "value between 50 and 250, total number of members where the gateway will stop sending offline members", defaulting to 50. On top of it sit graded rules: above 75,000 members a guild sends only members in voice, member chunks arrive "up to 1000 members per chunk", prefix queries return at most 100, and since August 2025 a request for every member in a guild is limited to one per guild per bot every 30 seconds. Discord's own framing of these limits, "due to our privacy and infrastructural concerns with this feature", is the clearest statement in the record that the two forces are travelling together.

Reconnect steering was added late and enforced softly. On 9 August 2022 the ready payload gained resume_gateway_url, a session-specific address for resumption. The announcement is unusual: rather than a cut-off date, it promises that apps not using the field "will be disconnected significantly faster than normal", while the field itself kept returning the generic address for a transition period. That is adoption pressure applied through quality of service rather than through breakage. Paired with it, a merged documentation change in May 2024 clarified that the server's Reconnect opcode "can come at any time, even before hello", which is the platform reserving the right to move a consumer off a host whenever it needs to.

Discord: declare at the handshake

A bitfield sent with IDENTIFY, three members of it gated behind review, enforced with close code 4014 if an app asks for something it has not been granted. Routing decisions can be made per connection before dispatch.

Source: gateway.mdx

Matrix: filter per request, best effort

The Client-Server specification enables lazy loading through lazy_load_members on a room event filter, and states plainly that it "is not intended to be a perfect optimisation" and that redundant membership events are valid. Cheaper to implement, weaker as a cost control.

Source: matrix-spec

Bluesky: a separate projection

Jetstream is a standalone service that consumes the firehose and re-serves it as filterable JSON, designed to be "dead-simple and cheap for us and others to operate on a single server". The narrowing lives outside the protocol, so the protocol never has to break.

Source: Jetstream design document

Underneath the contract sits the reason for it. Discord's published Elixir libraries are the only window the repository record gives onto the tier that does the fanout, and they are consistent about where the money goes. Manifold exists because "send calls cost about 70 µs/op so doing them in a loop eventually gets too expensive", with Discord running "a single GenServer per Discord server and some of these ~100,000 PIDs connected to them from many different Erlang nodes"; batching sends by destination node halved packets per second. ZenMonitor exists because when a process with many remote watchers dies, the notifications themselves are a stampede. SortedSet moved its core data structure into Rust through a native implemented function when copying terms into a growing vector "proved to be a performance bottle neck". Each of those is the server-side lever from Figure 1, and each has a floor. The contract is the lever without one.

03

The decisions that matter

Four forks with a recorded answer, and the condition that would flip each one.

Decision: where does the subscription filter live?

Chosen
  • Discord: a bitfield declared in the handshake, mandatory from API v8 (24 Sep 2020)
  • Lets the platform decide routing before dispatch, and makes the subscription auditable per app
Rejected
  • Per-request, best-effort filtering, which is what Matrix specifies for lazy loading
  • Discord back-applied intent restrictions to v6 on 27 Oct 2020 rather than leaving a filtered and an unfiltered path in service
Flips when
  • Your consumers are first-party or few, so a best-effort filter costs nothing to police
  • Or subscriptions change often within a session, which a handshake-time declaration handles badly

Decision: who computes the partitioning?

Chosen
  • The consumer, using a published formula over an immutable id, with a 2,500-guild ceiling per connection
  • No per-consumer placement state on the platform, and the consumer can resize its own fleet
Rejected
  • Server-assigned routing, which would have made the platform the owner of every consumer's topology
  • Large apps are instead migrated into a stricter regime by hand, with "a system DM and email confirming this move"
Flips when
  • Consumers cannot be trusted to shard correctly, at which point you are writing the client anyway
  • Or the partition key is mutable, which makes a published formula unusable

Decision: how do you take a capability back?

Chosen
  • Ship the pull-shaped substitute first (interactions, 15 Dec 2020), then gate the push-shaped capability (message content, 1 Sep 2022)
  • Offer a grace period whose price is a freeze: opting in "will be prevented from joining any additional servers"
Rejected
  • A hard cut-off with no replacement path
  • Also rejected: leaving the old surface quietly in place, which the v6 back-application closed off
Flips when
  • The capability has no substitute that fits the consumer's shape, in which case the honest move is a price, not a deprecation
  • Or the liability is legal rather than economic, which shortens every runway

Decision: how precisely do you specify recovery?

Chosen
  • One sentence of policy. A contributor's pull request mapping close codes to resume-or-reconnect was closed unmerged on 1 Feb 2022 with "if you get a disconnect, try to resume, otherwise reconnect"
Rejected
  • A published table binding each close code to a recovery action, which would have frozen server-side behaviour that Discord evidently wanted to keep changing
Flips when
  • Consumers must implement compensating logic per failure class, for example when replay has a cost, at which point vagueness is paid for in incidents rather than saved

Figure 3 · The question the platform now asks on your behalf

specific

every member

on demand

real time

no

yes

Do you need
every member,
or specific ones?

Get Guild Member
or Search Guild Members
no intent

Do you need them
in real time?

Interaction payload
carries the member
no intent

Above 10,000
users?

Toggle intent in
the developer portal

Apply for review,
reapply annually

specific

every member

on demand

real time

no

yes

Do you need
every member,
or specific ones?

Get Guild Member
or Search Guild Members
no intent

Do you need them
in real time?

Interaction payload
carries the member
no intent

Above 10,000
users?

Toggle intent in
the developer portal

Apply for review,
reapply annually

Discord's 2026 guide turns the subscription decision into a triage tree whose terminal states are mostly "do not subscribe"; the dashed outcome is the only one that now goes through review. Reconstructed from "You Might Not Need a Privileged Intent", checked 2026-09-22.
Diagram source
DecisionChosenRejectedBecauseEvidence
Subscription filterBitfield at handshake, mandatoryBest-effort per-request filterFiltering before dispatch saves the fanout, not just the bandwidthChange log, 2020-09-24
PartitioningConsumer-computed from an immutable idServer-assigned placementNo per-consumer routing state; consumer can resize its own fleetgateway.mdx, sharding
Capability withdrawalSubstitute first, then gateHard cut-offA withdrawal without a substitute becomes an ecosystem outageChange log, 2020-12-15 and 2022-09-01
Adoption pressureDegraded service for non-adoptersA dated breaking cut-offKeeps the old field working while making it worse to rely onChange log, 2022-08-09
Recovery semanticsOne sentence, no close-code tablePer-code resume matrixKeeps server behaviour free to changePR #4376, closed unmerged
Wire formatConsumer chooses JSON or ETF, zlib or zstdOne mandatory encodingCompression is consumer CPU against platform bandwidth, so let the consumer price itPR #6877, merged 2024-05-23
Version clarityKeep the joke about the skipped v7Document it plainlyBrand voice preferred over a documentation fix, over contributor objectionPR #2127, closed unmerged
04

What broke in production

Four published failures, in three classes: the reconnect herd, unbounded hydration, and the contract that leaked. Each is dated, and each produced a contract change.

A caveat that belongs at the top of this section rather than the bottom. Discord publishes incident write-ups on its website, and the research environment for this guide could not reach it, so nothing here is drawn from a formal postmortem. What follows comes from dated change-log entries in which Discord describes a failure and the change it made in response, and from issue threads in the client libraries where the same failures surface as user-visible errors. That is weaker evidence than a postmortem and it is stronger than inference, because in each case the platform is describing its own problem in its own words.

Reported

Connections that time out for the largest consumers

AssumptionAn app could keep scaling the number of guilds behind one shard fleet as long as it stayed inside the published ceiling.
What happenedDiscord reports "sessions have higher frequency of errors when starting if a bot has joined too many guilds (the gateway connection times out)". Start-up, not steady state, is where the cost concentrates: every shard's first connection has to hydrate guild state.
Blast radiusNot quantified publicly. Affects only apps near the large-bot threshold, which are the apps with the most users behind them.
FixOn 15 Mar 2021 the large-bot sharding threshold was lowered to 150,000 guilds "in order to improve reliability", moving more consumers into the regime with bucketed concurrency and a larger session budget.
Design ruleWhen start-up is the expensive path, your capacity control belongs on admission, not on steady-state throughput. Lowering the threshold at which stricter admission applies is a capacity lever you can pull without deploying anything.
Reported

Bulk member requests metered mid-flight

AssumptionAsking for every member of a guild is an ordinary operation that an app may repeat whenever its cache looks stale.
What happenedDiscord introduced a limit of one all-member request per guild per bot every 30 seconds, noting that "a small number of applications are currently exceeding this rate limit".
Blast radiusAnnounced 14 Aug 2025 for general rollout on 1 Oct 2025, but applied immediately to apps requesting all members in very large guilds "so we can ensure platform stability".
FixA new RATE_LIMITED dispatch carrying retry_after and the offending opcode, rather than a disconnect. The failure is now a typed event the consumer can handle.
Design ruleWhen you meter an operation consumers already depend on, return a structured refusal with a retry hint on the same channel. A disconnect forces a full reconnect, which costs you more than the request you refused.
Reported

Every app could see every channel, including the ones it could not open

AssumptionSending an app the full channel list is harmless metadata, because the app cannot read the messages inside channels it lacks permission for.
What happenedDiscord states the position plainly: "Today, bots receive every channel in a guild, including ones they can't view, with full metadata over both the Gateway and the HTTP API."
Blast radiusEvery guild with an installed app, for the life of the contract. The announcement is dated 12 Aug 2026, nine years into the published record.
FixObfuscation rather than removal on the gateway: the name becomes "___hidden___", sensitive fields are nulled, a CHANNEL_OBFUSCATED flag (1 << 17) is set, and the HTTP listing omits the channel entirely.
Design ruleStructural metadata leaks outlive content leaks, because nobody classifies a channel list as data. Audit what your push contract sends before authorization is applied, not only what it sends after.
Reported

"The WebSocket rate limit has been hit, this should never happen"

AssumptionPer-connection metering is invisible to an app that is not obviously abusing the gateway.
What happenedA discord.js user hit the 120-events-per-60-seconds ceiling during shard start-up; the shard was destroyed with close code 4008 and reconnected, after which logs show "Members didn't arrive in time". Hydration traffic collided with the meter.
Blast radiusOpened 14 Jan 2024 against discord.js 14.10.2. Closed through library PR #10098: the fix landed in the library, not in the app.
FixClient-library scheduling of gateway commands, which is where the platform's meter is actually enforced for most consumers.
Design ruleIf a handful of libraries wrap your protocol, your rate limits are a library-scheduling problem. Publish the budget in machine-readable form and expect the libraries, not the apps, to be your real counterparty.

Figure 4 · Why a fanout platform fears the reconnect, not the steady state

"Guild processes""Gateway""Consumer fleet (Nshards)""Guild processes""Gateway""Consumer fleet (Nshards)""Bucketed: max_concurrency per 5sBudget: 1000 per 24h""1 per guild per 30ssince Aug 2025""Reconnect (may arrive anytime)""IDENTIFY x N""hydrate guild state""GUILD_CREATE, chunked""READY + GUILD_CREATE xguilds""Request Guild Members (op 8)""RATE_LIMITED (retry_after)"
"Guild processes""Gateway""Consumer fleet (Nshards)""Guild processes""Gateway""Consumer fleet (Nshards)""Bucketed: max_concurrency per 5sBudget: 1000 per 24h""1 per guild per 30ssince Aug 2025""Reconnect (may arrive anytime)""IDENTIFY x N""hydrate guild state""GUILD_CREATE, chunked""READY + GUILD_CREATE xguilds""Request Guild Members (op 8)""RATE_LIMITED (retry_after)"
The expensive path is re-entry: identify, hydrate, and do it for every shard at once. The three controls on the right are the ones Discord added between 2018 and 2025. Reconstructed from gateway.mdx and the change log.
Diagram source

The fourth failure class does not show up in Discord's own record at all, and it is the one an architect should care most about, because it is the cost the platform exports. Measured in the package registries, the two largest client libraries reorganise themselves around every contract change. discord.js shipped v12 on 1 March 2020, two weeks after intents were documented; v13 on 6 August 2021 alongside API v9; v14 on 17 July 2022, six weeks before message content became privileged. discord.py stopped publishing after 1.7.3 on 12 June 2021 and published nothing for 432 days, returning with 2.0.0 on 18 August 2022, two weeks before the message content deadline. The registry does not say why, and I am not going to claim a cause it does not record. What it does show is the shape of the exported cost: a contract change is a fleet-wide migration carried out by volunteers, and the platform's deadline is enforced against them first.

05

Numbers you can plan against

Everything quantitative in this guide, with the date it was true and the artefact it came from.

MetricValueAtContextAs ofSource
Guilds per gateway connection2,500DiscordHard cap; above it sharding is mandatory2026-09gateway.mdx
Large-bot sharding threshold150,000 guildsDiscordSet at over 100,000 in Jan 2018, lowered to 150,000 in Mar 2021; the intervening value is not in the record2026-09change log
Identify budget1,000 / 24hDiscordGlobal across shards; exceeding it resets the bot token2026-09gateway.mdx
Identify budget, large botsmax(2000, guilds/1000 × 5)DiscordDerived from guild count, so capacity to restart scales with size2026-09gateway.mdx
Gateway command budget120 / 60sDiscordPer connection; exceeding it disconnects immediately2026-09gateway.mdx
All-member request limit1 per guild per 30sDiscordAnnounced 14 Aug 2025, general rollout 1 Oct 20252025-08change log
Member chunk size1,000DiscordResponse granularity for member hydration2026-09gateway-events.mdx
Offline-member cut-off50 to 250 (default 50)Discordlarge_threshold in the identify payload2026-09gateway-events.mdx
Guild size above which only voice members are sent75,000DiscordApplies when the presences intent is absent2026-09gateway-events.mdx
Privileged intent review threshold10,000 usersDiscordReplaced a 100-server threshold; annual reapplication2026-06change log
Cost of one remote send on the BEAM~70 µsDiscordThe reason batching by destination node existed at all2017-02Manifold README
Processes attached to one guild~100,000DiscordSingle GenServer per guild, sessions attached from many nodes2017-02Manifold README
Shared-config read cost0.33 µs vs 7.64 µs (ETS)DiscordFastGlobal benchmark; the technique became persistent_term in OTP 21.22017-02FastGlobal README
Change-log entries per year8 (2017) to 59 (2026)DiscordCounted from the 237 dated entries; 2026 is partial, to 18 September2026-09change log
Client-library silence432 daysdiscord.pyBetween 1.7.3 (2021-06-12) and 2.0.0 (2022-08-18)2026-09PyPI
Published Elixir fanout library, releases10 over 9 yearsDiscordManifold, 2017-02-20 to 2026-07-07; 646,908 downloads2026-09Hex
Read these carefully

Every figure above is a published parameter or a registry timestamp, not a measurement of Discord's production systems. Nothing here tells you Discord's event rate, connection count, machine count or cost, because none of that appears in the repository record. The two microsecond figures are 2017 benchmarks published by Discord on hardware it does not describe, and they are useful as ratios rather than as absolutes. The large-bot threshold row is the one to re-check first: the change log records it moving twice and the current documentation carries only the latest value.

Figure 5 · Nine years of narrowing, in the order it was announced

2017 · v6 schema break;
everything below v6 discontinued

2018 · very large bot sharding
above 100,000 guilds

2020 · intents documented (Feb),
mandatory in v8 (Sep),
interactions ship (Dec)

2021 · large-bot threshold
lowered to 150,000 for reliability

2022 · resume_gateway_url,
message content becomes privileged

2024 · zstd-stream documented,
Reconnect may arrive any time

2025 · all-member requests metered,
guild creation withdrawn

2026 · intent review per 10,000 users,
channel obfuscation,
shard param required on HTTP

2017 · v6 schema break;
everything below v6 discontinued

2018 · very large bot sharding
above 100,000 guilds

2020 · intents documented (Feb),
mandatory in v8 (Sep),
interactions ship (Dec)

2021 · large-bot threshold
lowered to 150,000 for reliability

2022 · resume_gateway_url,
message content becomes privileged

2024 · zstd-stream documented,
Reconnect may arrive any time

2025 · all-member requests metered,
guild creation withdrawn

2026 · intent review per 10,000 users,
channel obfuscation,
shard param required on HTTP

Read top to bottom, the arc runs from schema breaks to capacity controls to access review, and the year that ships a substitute (2020, heavy border) precedes every year that takes something away (2022, 2025, 2026, dashed). Dates from the change log, checked 2026-09-22.
Diagram source
06

The evidence wall

Every source behind this page, graded. The mix is unusual and the reason is stated below.

What is missing, and why

There are no engineering-blog, conference-talk or paper sources in this guide. The research environment's network policy allowed GitHub, GitLab and the public package registries and nothing else, so discord.com, its status page, video hosts and paper archives were all unreachable. Everything below is therefore primary in a narrow sense, documentation source, change-log entries, pull requests, issue threads, library READMEs and release timestamps, and thin in a specific way: no independent measurement of Discord's systems appears anywhere in it. Treat the mechanism as well evidenced and the magnitudes as unverified.

Decision record Discord2017-07 to 2026-09

Developer change log, 237 dated entries

The spine of this guide. Each entry is dated, tagged by surface, and written at the time of the decision. Twenty-seven are tagged Breaking Change, and the yearly count rises from eight in 2017 to fifty-nine in the first nine months of 2026.

Carry forwardA tagged, dated change log is the cheapest architecture record a platform can keep, and it is auditable by anyone.
raw.githubusercontent.com/discord/discord-api-docs/main/developers/change-log.mdx
Reported failure Discord2021-03-15

Large bot sharding lowered to 150,000 guilds

Discord reports gateway connections timing out for apps in too many guilds and responds by moving more apps into the stricter admission regime. The only entry in the record that names a reliability symptom and its remedy in the same paragraph.

Carry forwardAdmission thresholds are a capacity lever you can pull without shipping code.
Change log entry, 15 March 2021
Reported failure Discord2025-08-14

Rate limit on requesting all guild members

One request per guild per bot every 30 seconds, applied early to the largest guilds "so we can ensure platform stability", with a typed RATE_LIMITED dispatch instead of a disconnect.

Carry forwardRefuse expensive requests on the same channel, with a retry hint, rather than dropping the connection.
Change log entry, 14 August 2025
Reported failure Discord2026-08-12

Channel obfuscation for users and bots

An admission that the gateway has always sent apps every channel in a guild with full metadata, including channels the app cannot view, and a description of the obfuscation that replaces it.

Carry forwardAudit what your push contract emits before authorization, not only after it.
Change log entry, 12 August 2026
Reported failure discord.js2024-01-14

Issue #10089: the WebSocket rate limit has been hit

Gateway metering surfacing as close code 4008 and shard destruction during start-up, followed by "Members didn't arrive in time". Fixed in the library's scheduling, not in the app.

Carry forwardFor a protocol wrapped by a few libraries, the libraries are your real rate-limit counterparty.
github.com/discordjs/discord.js/issues/10089
Documentation Discordchecked 2026-09-22

Gateway documentation: sharding, admission, metering

The published formula shard_id = (guild_id >> 22) % num_shards, the 2,500-guild ceiling, bucketed identify concurrency, the 1,000-per-day session budget with token reset, and the 120-commands-per-60-seconds meter.

Carry forwardPublishing the routing formula is what lets you delegate partitioning without holding placement state.
developers/events/gateway.mdx
Documentation Discordchecked 2026-09-22

Gateway events: request guild members, large_threshold

The hydration rules, graded by guild size, and the sentence that names both motives at once: "Due to our privacy and infrastructural concerns with this feature, there are some limitations that apply."

Carry forwardA limit that serves privacy and capacity together is the one that survives review.
developers/events/gateway-events.mdx
Documentation Discordchecked 2026-09-22

You might not need a privileged intent

A guide whose purpose is to talk developers out of subscribing: look the member up by id, search by prefix, or take the member object that already arrives inside an interaction.

Carry forwardThe cheapest subscription is the one the consumer decides it does not need.
developers/gateway/you-might-not-need-a-privileged-intent.mdx
Decision record Discord2020-09-24

API and Gateway v8: intents become mandatory

The version bump that converted an optional saving into a requirement, removed guild_subscriptions, and was followed six weeks later by back-applying the restrictions to v6.

Carry forwardAn optional efficiency feature becomes a control only when the old path is closed.
Change log entry, 24 September 2020
Decision record Discord2022-09-01

Message content is a privileged intent

The largest withdrawal in the record, with a grace period priced as a growth freeze: apps opting in "will be prevented from joining any additional servers until you opt-out".

Carry forwardIf you must extend a deadline, charge for the extension in something the consumer values.
Change log entry, 1 September 2022
Decision record Discord2022-08-09

Session-specific gateway resume URLs

Adoption driven by degraded service rather than a cut-off: non-adopters "will be disconnected significantly faster than normal" while the field still returns the old address.

Carry forwardQuality of service is a migration lever that does not break anyone on day one.
Change log entry, 9 August 2022
Decision record Discord2026-06-10

Privileged intent access moves to a user threshold

Review is triggered at 10,000 users rather than 100 servers, access must be renewed annually, and apps may keep growing while under review.

Carry forwardGate on the quantity that actually drives your cost and your exposure, not on a proxy for it.
Change log entry, 10 June 2026
Decision record Discord2020-10-04

PR #2127, "More serious v7 status?", closed unmerged

A documentation fix for the skipped v7 rejected the day it was opened, on brand grounds, over contributor objections that "the whole v6/v7 thing is repeatedly a cause for confusion".

Carry forwardVersion numbering is contract surface; a joke in it is paid for by every new integrator.
github.com/discord/discord-api-docs/pull/2127
Decision record Discord2022-02-01

PR #4376, close-code resume matrix, closed unmerged

A contributor proposed documenting which close codes are resumable. Discord closed it with "if you get a disconnect, try to resume, otherwise reconnect", keeping recovery policy deliberately coarse.

Carry forwardEvery documented failure mapping is a constraint on your future behaviour; refuse deliberately, not accidentally.
github.com/discord/discord-api-docs/pull/4376
Source Discord2024-05-23

PR #6877, zstd-stream documented, Reconnect clarified

Documents "the resurrected zstd-stream gateway compression option" and clarifies that the Reconnect opcode "can come at any time, even before hello".

Carry forwardReserve the right to evict a connection in the contract, or you will never be able to rebalance.
github.com/discord/discord-api-docs/pull/6877
Source Discord2017-02 to 2026-07

Manifold: batching sends by destination node

"Send calls cost about 70 µs/op"; a single GenServer per guild with around 100,000 attached processes; packets per second halved after deployment. Ten releases over nine years, the most recent in July 2026.

Carry forwardPer-recipient send cost is the number that decides whether your fanout tier scales; measure it before designing around it.
raw.githubusercontent.com/discord/manifold/master/README.md
Source Discord2017-02-20

FastGlobal, and the release that never came

One release, ever, and 766,789 downloads. Reading shared configuration at 0.33 microseconds against 7.64 for ETS, by compiling data into a module at runtime.

Carry forwardA workaround that the platform later absorbs is the best possible outcome for a published library; plan for it to stop.
hex.pm/api/packages/fastglobal
Source Erlang/OTP2018-11-06

persistent_term, and PR #1989 that added it

ERTS 10.2 added "a term storage suitable for terms that are frequently used but never or infrequently updated", with constant-time lookups and no copying. The pull request names mochiglobal and "live-compiled modules as data stores" as the patterns it replaces.

Carry forwardWhen several large users ship the same workaround, the runtime is where the fix belongs.
github.com/erlang/otp/pull/1989
Source Discord2019-05 to 2025-12

SortedSet: the hot data structure moved to Rust

A native implemented function in Rust, buckets of 500 by default, written because copying terms into a growing vector "proved to be a performance bottle neck". Still changing in December 2025, six years after its first release.

Carry forwardMoving one data structure into native code is a smaller change than moving a workload off its runtime, and it buys years.
hex.pm/api/packages/sorted_set_nif
Source Discordchecked 2026-09-22

ZenMonitor: making failure notification cheap

A drop-in replacement for Process.monitor/1 that batches and throttles the notifications produced when a heavily watched process dies, at the cost of an ETS row per monitor.

Carry forwardIn a fanout system, the death of a hub is itself a fanout event; budget for it.
raw.githubusercontent.com/discord/zen_monitor/master/README.md
Source Discord2017-02 to 2021-12

Erlpack: the internal encoding, published

An encoder for Erlang term format 131 in C++ with Python and JavaScript bindings, which is what the gateway's ETF option actually is. Five npm versions, the last in December 2021.

Carry forwardOffering your internal wire format to consumers saves you a translation layer and ties them to your runtime's data model.
registry.npmjs.org/erlpack
Source Discord2018-02 to 2025-06

lilliput: the same instinct on the media path

An image resizing library written to do "as little memory allocation as possible and especially not to create garbage in Go". Published v1.0.0 in February 2018, then nothing until v1.3.0 in September 2024, then three releases in nine months.

Carry forwardA long publication gap followed by a burst usually marks a format or product change, not a rewrite.
proxy.golang.org/github.com/discord/lilliput/@v/list
Source Rapptz2015-08 to 2026-03

discord.py release history on PyPI

Eighty-seven releases, with a 432-day gap between 1.7.3 on 12 June 2021 and 2.0.0 on 18 August 2022, which closes two weeks before the message content deadline.

Carry forwardRegistry cadence is the cheapest health signal you have for the ecosystem your contract depends on.
pypi.org/pypi/discord.py/json
Source discord.js2015-08 to 2026-09

discord.js major versions on npm

v12 on 1 March 2020, v13 on 6 August 2021, v14 on 17 July 2022: each major lands within weeks of a contract change, which is what the migration actually costs.

Carry forwardCount your deprecation runway from the library's release cadence, not from your own announcement date.
registry.npmjs.org/discord.js
Specification Matrix.orgchecked 2026-09-22

Client-Server API: lazy-loading room members

The same hydration problem solved with a per-request filter that the specification explicitly allows to be imprecise: redundant membership events are valid "to ease implementation".

Carry forwardA best-effort filter is cheap to specify and weak as a cost control; know which you are buying.
matrix-spec client-server-api
Source Blueskychecked 2026-09-22

Jetstream: narrowing outside the protocol

A standalone service that re-serves the firehose as filterable JSON, designed to be "dead-simple and cheap for us and others to operate on a single server", with at-least-once delivery and idempotent consumers as an explicit non-goal list.

Carry forwardIf your protocol cannot be changed, put the narrowing in a service beside it and let consumers choose.
Jetstream design document
Source Discordchecked 2026-09-22

discord-api-spec: the machine-readable contract

An OpenAPI 3.1 specification covering "only the most recent version (v10)", generated automatically, closed to public contributions, with a preview file that "is subject to breaking changes without advance notice".

Carry forwardPublishing a generated spec for the current version only is a way to be precise without promising stability.
github.com/discord/discord-api-spec
Case study Discord / ROOSTchecked 2026-09-22

Osprey: the other consumer of the same stream

Discord's internal safety rules engine, "originally developed internally at Discord to combat spam, abuse, botting, and scripting", open-sourced through a non-profit and listed as adopted by Bluesky and Matrix.org as well as Discord.

Carry forwardThe abuse engine is a first-class consumer of your event stream, and it wants the firehose that third parties are being weaned off.
raw.githubusercontent.com/discord/osprey/main/README.md
07

Build a miniature, then productionise it

Six rungs. The line between a toy and a platform is crossed at rung four, where you first refuse a consumer something it wants.

Push everything, and measure the bill

A WebSocket server, an in-memory set of entities, and a fanout loop. Attach 500 simulated consumers to 50 entities and record bytes sent per consumer per second and the server CPU spent per delivered event.

Done when: you can state the cost of one event delivered to one consumer.  Teaches: fanout cost is a product, and you cannot reason about the contract until you can price it.

Add a declared subscription at the handshake

A bitfield in the connect message. Refuse anything not declared. Re-run the same load and compare, then try the other design: filter after fanout instead of before it.

Done when: both filter positions are measured on the same workload.  Teaches: why Discord made the declaration part of IDENTIFY rather than a per-request filter.

Delegate partitioning with a published formula

Give consumers shard_id = f(entity_id) % num_shards and a per-connection entity ceiling. Run one consumer fleet of 4 shards and one of 8 against the same data, with an overlap window, and cut over without dropping events.

Done when: the cutover is invisible to the consumer's own users.  Teaches: routing derived from an immutable id costs the platform nothing to support.

Meter the expensive request, and refuse it properly

Add a bulk state request. Meter it per entity per consumer, and answer over-limit calls with a typed refusal carrying retry_after rather than closing the connection. Then kill the whole fleet and watch what re-entry costs you.

Done when: a full fleet restart completes within a budget you set in advance.  Teaches: re-entry, not steady state, is what your capacity plan has to survive.

Add admission control and reconnect steering

Bucket connection attempts by shard id, give each consumer a daily session budget, and hand back a session-specific resume address. Simulate an eviction of 30 per cent of connections and measure how long full re-entry takes with and without the buckets.

Done when: you can show the herd flattening, with a number.  Teaches: admission control is the only lever that works when every consumer reconnects at once.

Take a capability away

Pick something consumers rely on. Ship the pull-shaped substitute first, publish a dated entry, give a grace period with a price attached, then enforce. Track the release cadence of your three biggest consumer libraries through the whole window.

Done when: the substitute's adoption curve, not your announcement, sets the enforcement date.  Teaches: the deprecation you can actually execute is the one somebody else has already migrated to.

08

Keep hunting

The queries that produced this page, in an environment where only code hosts and package registries were reachable. They work better than blog searches even when blogs are open.

Mine the contract itself

  • curl -s https://raw.githubusercontent.com/<org>/<docs-repo>/main/<path>/change-log.mdx | grep -c '<Update'
  • grep -o 'label="[^"]*"' change-log.mdx | sort | uniq -c
  • grep -B2 -A40 'Breaking Change' change-log.mdx

Find the arguments, not the answers

  • https://github.com/<org>/<repo>/pulls?q=is%3Apr+is%3Aclosed+is%3Aunmerged+sort%3Acomments-desc
  • https://github.com/<org>/<repo>/issues?q=is%3Aissue+sort%3Acomments-desc
  • https://github.com/<org>/<repo>/pulls?q=is%3Apr+<protocol-term>

Date everything from the registries

  • curl -s https://hex.pm/api/packages/<pkg> | jq '.releases[] | {version, inserted_at}'
  • curl -s https://pypi.org/pypi/<pkg>/json | jq -r '.releases | to_entries[] | [.value[0].upload_time, .key] | @tsv' | sort
  • curl -s https://proxy.golang.org/<module>/@v/list
  • curl -s https://registry.npmjs.org/<pkg> | jq '.time'

Compare against the other networks

  • grep -n "lazy_load\|filter" matrix-spec/content/client-server-api/_index.md
  • raw.githubusercontent.com/bluesky-social/jetstream/main/docs/README.md
  • site:github.com "rate limit" "gateway" path:docs protocol
09

References

  1. Discord, Developer Change Log discord/discord-api-docs, entries dated 2017-07-19 to 2026-09-18. Checked 2026-09-22.
  2. Discord, Gateway documentation discord/discord-api-docs, developers/events/gateway.mdx. Checked 2026-09-22.
  3. Discord, Gateway Events documentation discord/discord-api-docs, developers/events/gateway-events.mdx. Checked 2026-09-22.
  4. Discord, You Might Not Need a Privileged Intent discord/discord-api-docs. Checked 2026-09-22.
  5. Discord, discord-api-docs repository GitHub. Checked 2026-09-22.
  6. Discord, PR #2127 "More serious v7 status?" Closed unmerged 2020-10-04. Checked 2026-09-22.
  7. Discord, PR #4376 "Clarify Which Gateway Close Codes to Resume on" Closed unmerged 2022-02-01. Checked 2026-09-22.
  8. Discord, PR #6877 "[gateway] Clarify Reconnect opcode, Document zstd-stream" Merged 2024-05-23. Checked 2026-09-22.
  9. Discord, discord-api-spec (OpenAPI 3.1) GitHub. Checked 2026-09-22.
  10. Discord, Manifold README discord/manifold. Checked 2026-09-22.
  11. Manifold release history Hex, 2017-02-20 to 2026-07-07. Checked 2026-09-22.
  12. Discord, FastGlobal README discord/fastglobal. Checked 2026-09-22.
  13. FastGlobal release history Hex, single release 2017-02-20. Checked 2026-09-22.
  14. Discord, ZenMonitor README discord/zen_monitor. Checked 2026-09-22.
  15. Discord, SortedSet NIF README discord/sorted_set_nif. Checked 2026-09-22.
  16. SortedSet NIF release history Hex, 2019-05-10 to 2025-12-04. Checked 2026-09-22.
  17. rustler crate index crates.io index, 58 versions. Checked 2026-09-22.
  18. Erlpack release history npm, 2017-02-17 to 2021-12-22. Checked 2026-09-22.
  19. Discord, lilliput README discord/lilliput. Checked 2026-09-22.
  20. lilliput module versions Go module proxy, v1.0.0 (2018-02-09) to v1.5.0 (2025-06-23). Checked 2026-09-22.
  21. Discord and ROOST, Osprey README discord/osprey. Checked 2026-09-22.
  22. Discord, Access README discord/access. Checked 2026-09-22.
  23. Discord, rules_elixir README discord/rules_elixir, fork of RabbitMQ's Bazel rules. Checked 2026-09-22.
  24. Discord, react-native fork GitHub, forked from react/react-native. Checked 2026-09-22.
  25. Erlang/OTP, ERTS release notes ERTS 10.2, persistent_term. Checked 2026-09-22.
  26. Erlang/OTP, PR #1989 "RFC: Add a persistent term storage" Merged 2018-11-06. Checked 2026-09-22.
  27. Matrix.org, Client-Server API specification matrix-org/matrix-spec. Checked 2026-09-22.
  28. Bluesky, Jetstream design document bluesky-social/jetstream. Checked 2026-09-22.
  29. discord.py release history PyPI, 2015-08-23 to 2026-03-03. Checked 2026-09-22.
  30. discord.py 2.0.0 source distribution PyPI files, uploaded 2022-08-18. Checked 2026-09-22.
  31. discord.js release history npm, 2015-08-10 onward. Checked 2026-09-22.
  32. discord.js, issue #10089 Opened 2024-01-14. Checked 2026-09-22.