Want it harder? Hide the model answer and work each phase in a scratchpad against its time budget, then reveal and score yourself. Or simply read on: the checkpoints below work either way.
How Do You Tell Seventy Thousand People Something at Once, and Let Any of Them Start Talking?
It is a Sunday night in October 2026 in Pune. Ishaan Kulkarni, 24, is the head moderator of Lobby 91, a Discord server for Indian esports fans with four lakh members. Tonight is the grand final of the server's own BGMI tournament. By 8:45 pm the member list on the right of his screen reads "Online — 71,204". Sixteen team voice channels sit under a "TEAMS" category, each showing the four avatars inside it; the casters are live in the stage channel; and #live-chat is scrolling faster than anyone can read.
At 8:55 he posts in #announcements: "Grand final starts in 5 minutes. Casters are live on stage. @everyone". Within a second the message is on seventy thousand screens, and phones start buzzing for hundreds of thousands of members who aren't even online. Two minutes later #live-chat fills with fresh accounts posting the same "free UC" link. Ishaan turns slowmode on, pauses invites and bans the accounts. Then a team captain messages him: the other team's player is using a banned device. Ishaan clicks "Team Lobby 3", the voice channel goes green, and he is talking to four people in Hyderabad before he has finished settling his headset.
Nothing Ishaan did that night needed a meeting link, a call to answer or a refresh. The server, its channels, its member list and its voice rooms were simply there, live, for everyone at once.
- 90 million+ daily active users (Discord company page, "Discord Internal Data, as of Q4 2025"); "roughly 200 million people who use our platform every month" (Discord Engineering, September 2024)
- Nearly 5 million concurrent users and "millions of events per second" (Discord Engineering, July 2017); "up to 500,000 live sessions" on one Erlang VM (same post)
- 10 million+ members and 1 million+ online in Midjourney's server; individual servers "approaching two million concurrent users"; up to 15,000 sessions per relay; about 90% of user-guild connections in large servers are passive (Discord Engineering, October 2023)
- 2.6 million+ concurrent voice users on 850+ voice servers in 13 regions, 220+ Gbps and 120 million packets a second of egress (Discord Engineering, September 2018); 25,000+ SFU instances (April 2026); 80%+ of voice and video traffic on Cloudflare's edge (June 2026)
- 177 Cassandra nodes replaced by 72 ScyllaDB nodes for trillions of messages; copied in days, not months (the Rust migrator's estimate was 9 days) at up to 3.2 million messages a second; p99 history reads from 40–125 ms to 15 ms (Discord Engineering, March 2023)
- Almost 40% less gateway bandwidth from zstandard compression and delta "passive" updates (Discord Engineering, September 2024)
For context:
Discord's servers are called "guilds" inside its code; this quest uses both words, and "server" for a machine only where it's clear
Ishaan, Lobby 91, its tournament and every number about them are illustrative
Discord's engineers have written unusually openly about the gateway, guild processes, message storage and voice; the numbers above come from posts years apart, and each is dated. Everything else here, and every number labelled as an assumption, is an illustrative design
This is the system we'll design today.
Discord counts 90 million+ daily active users (Q4 2025), and Midjourney's server has 10 million+ members with 1 million+ online at once (2023). Fan-out is quadratic: 1,000 people online each saying one thing is 1 million notifications, 100,000 online is 10 billion. On stated assumptions the peak is ~31 million sessions, ~50,000 messages a second and ~6 million dispatches a second. A message goes in over HTTP, is stored in ScyllaDB before anyone is told, and is dispatched to the server's guild process, one Elixir process per server that orders its events and fans them out through relays of up to 15,000 sessions to gateways holding a session per device; about 90% of connections in large servers are passive and get summaries instead. Session buffers are capped and reconnects resume from a sequence number; presence goes only to sessions viewing that part of the member list. Messages are keyed by channel and a ~10-day bucket behind data services that coalesce reads; the move from 177 Cassandra nodes to 72 ScyllaDB nodes took days, not months, and cut p99 reads from 40–125 ms to 15 ms. Voice is a separate plane: in 2018 a guild's voice channels shared one voice server, in 2026 voice syncers place each call on one of 25,000+ SFUs, and every call outside stage channels is end-to-end encrypted. The edge cases are a tournament final, a spam raid and the 25 March 2026 outage that stopped 17% of sessions at once.
The Interview Begins#
You're interviewing at a company that builds community software. The interviewer shares a screenshot: a column of server icons on the far left, a channel list with a speaker icon next to "Team Lobby 3" and four small avatars under it, a chat scrolling in the middle, and on the right a member list headed "Online — 71,204".
"Every part of this screen is live. The avatars under the voice channel, the online count, the member list, the chat. Design the platform: servers, channels, the real-time delivery, presence, message history, voice channels and the rate limits that keep it alive. One server on this screen has seventy thousand people online; another has a million."
Users belong to many servers. Each server has text and voice channels, roles and per-channel permissions, and from a handful to millions of members. Every connected client receives, in real time, every event it's allowed to see: messages, edits, reactions, typing, members joining, presence and who is in which voice channel. Clicking a voice channel connects you to everyone in it within about a second. Message history is kept forever and can be scrolled back to the first message. The platform must survive huge servers, sudden pings to everyone, spam raids and mass reconnects.
Requirements
- Servers with channels, roles and permissions; up to millions of members in one server
- Send a message and deliver it to every online member who can see the channel
- Show presence (online, idle, do not disturb, activity) and a live member list
- Drop-in voice channels with video and screen sharing
- Keep every message forever; load recent history instantly, jump to any old message
- Rate limits and anti-abuse for users, bots and whole servers
"One-to-one messaging, delivery receipts and offline queues are a different problem; assume you've seen how a messenger does them. Same for scheduled video meetings and recording. Leave out payments, search ranking and the bot marketplace. I want the community: big rooms, live state, voice you can walk into."
Phase 1: Requirements Clarification (5 minutes)#
"A messenger delivers a message to a few people, once. Here one action can go to a million screens, and the hard part is how the work grows with the size of a server. So I'll ask about sizes first, then about what 'live' has to mean for each kind of event."
Your Questions#
"How many users, and how many online at once?"
"Discord's company page gives 90 million daily active users (fourth quarter of 2025); in 2024 its engineers wrote of roughly 200 million a month. Its last concurrency figures are older: nearly five million in 2017, and a 2019 post titled for 11 million. Assume more today."
"How big does a single server get?"
"Discord wrote in October 2023 that Midjourney's server has over 10 million members, over a million of them online, each with a session connected to the guild, and that it had scaled individual servers 'from tens of thousands of concurrent users to approaching two million concurrent users in the past few years'."
"What has to happen when someone sends a message?"
"In Discord's words, 'we need to update the UI in the client of everyone who's online that is in the same server'. Some events go to everyone; others need a permission check per recipient."
"How long is history kept?"
"Forever. Discord decided early 'to store all chat history forever so users can come back at any time and have their data available on any device'. In January 2017 that was over 120 million messages a day; by 2022 the store held trillions."
"And voice?"
"Every voice channel is a room you can walk into. In 2018 Discord ran more than 850 voice servers in 13 regions for over 2.6 million concurrent voice users, and had seen '1000 people taking turns speaking' in one channel. Since early March 2026 every voice and video call outside stage channels is end-to-end encrypted."
Functional Requirements#
- Servers: create servers, text and voice channels, categories, roles; per-channel permission overrides decide who sees and does what
- Messages: send, edit, delete, react, reply, mention a user, a role or everyone; every online member who can see the channel gets it live
- History: kept forever; open a channel at the latest messages; jump to any message by id; track what each user has read and their mentions
- Presence: online, idle, do not disturb, invisible, and activities ("Playing BGMI"); a member list sorted by role and status, with online counts
- Voice: join a voice channel with one click; voice, camera and screen share (Go Live); server mute and move by moderators
- Safety: per-user and per-bot rate limits, slowmode per channel, automatic spam and raid defences
Non-Functional Requirements#
Scale (published and estimated)
- ~30 million concurrent sessions at peak (estimated below); servers with 1 million+ online (published)
- ~6 million dispatches a second to clients at peak (estimated below)
Latency (design targets)
- Message send to delivery on other members' screens: p99 < 500 ms in a normal server, < 2 s in the largest
- Click a voice channel to "Voice Connected": p95 < 1 s; mouth-to-ear < 200 ms within a region
Correctness
- A sent and acknowledged message is never lost; clients see each channel's events in order
- No event reaches a session that isn't allowed to see it
- Presence and member counts may lag by seconds; messages and permissions may not
Phase 2: Back of the Envelope Estimation (5 minutes)#
"My anchors are Discord's: 90 million daily actives, up to 500,000 sessions on one Erlang VM, an average user in five servers in 2017, a million online in one server, 15,000 sessions per relay, about 90% of connections in large servers passive, and the 2018 voice fleet. Discord hasn't published current concurrency or message rates, so those are assumptions, and I'll say so."
Before the numbers: why do big servers get expensive so fast?
Discord's own example: if 1,000 people are online in a server and each says "I love jello" once, that's 1 million notifications. Work out the same for 10,000 and 100,000 people online. Why does the work grow faster than the server? Then take Midjourney, with about 1 million online: if only 10% of those connections need every event, and each relay handles 15,000 sessions, how many deliveries does one message cost, and how many messages does the guild process itself have to send?
One strong answer
- Quadratic: each message goes to everyone online, and the number of messages also grows with everyone online. 10,000 online → 10,000 × 10,000 = 100 million; 100,000 online → 10 billion. Ten times the people is a hundred times the work.
- Passive sessions: 1 million online × 10% active = ~100,000 sessions that get the full message; the other 900,000 get an occasional summary instead.
- Relays: 1 million sessions ÷ 15,000 per relay ≈ 67 relays. The guild process sends one copy to each relay; the relays do the per-session permission checks and sends in parallel.
- So the guild process does ~67 sends per message, not a million, and the platform does ~100,000 deliveries, not a million.
Connections#
| Value | |
|---|---|
| Daily active users (Discord, Q4 2025) | 90 million+ |
| Concurrent users at the daily peak (assumed ~30% of DAU) | ~27 million |
| Sessions per online user (assumed: some on desktop and phone) | ~1.15 → ~31 million sessions |
| Sessions on one Erlang VM (Discord, 2017: "up to 500,000") | 500,000 |
| Session nodes if packed full / at ~50% to absorb reconnects | ~62 / ~125 |
| Heartbeats (assume one every ~45 s, the docs' example interval) | 31 million ÷ 45 ≈ ~690,000 a second |
Conclusion: the connections themselves are cheap to hold; what costs is everything that happens when many of them reconnect at once, and the events pushed down them.
Messages, Fan-Out and Storage#
- Messages: Discord published 120 million a day in January 2017 and "trillions" stored by 2022, but no current rate. Assume ~1.5 billion a day: ~17,000 a second on average, ~50,000 a second at a 3× peak
- Deliveries: assume ~40 active sessions per message on average → ~2 million message deliveries a second; reactions, typing, presence and voice states add about twice as many again → ~6 million dispatches a second, the same order as the "millions of events per second" Discord reported in 2017
- Reads: "about 50/50" reads to writes in 2017, and "extremely random": returning just 50 messages from a quiet channel can mean many random disk seeks
- Disk: ~1.5 billion × ~100 bytes compressed (assumed) × 3 replicas ≈ ~450 GB a day; for scale, Discord's 72 ScyllaDB nodes × 9 TB of disk each ≈ ~650 TB of raw disk (our arithmetic) for trillions of messages in 2023
Conclusion: the database sees tens of thousands of writes a second; the real-time layer sees millions of dispatches. Design the fan-out first.
One Huge Server and Presence#
- Events: assume ~50 a second across its channels. To 1 million online sessions that's 50 million permission checks and sends a second; to the ~100,000 active ones, ~5 million: still as much as the rest of the platform, so it can't run on one core
- Relays: the guild sends 50 × ~67 ≈ 3,350 messages a second; the relays do the rest in parallel
- Presence: assume each online user changes status every ~5 minutes. In this one server that's ~3,300 changes a second; sent to everyone online, 3.3 billion deliveries a second. Impossible
- Lazy member list: a client sees ~100 rows at a time (assumed); send a change only to sessions viewing that row, plus friends
Conclusion: a huge server is a hot key in time and in memory, and presence is only affordable if each client subscribes to what it can see.
Voice#
- 2018 (derived, our arithmetic): Discord published 220+ Gbps and 120 million+ packets a second of egress for 2.6 million+ users. 220 Gbps ÷ 2.6 million ≈ ~85 kbit/s and 120 million ÷ 2.6 million ≈ ~46 packets a second per user: about one audio stream of 20 ms frames. Both totals are "more than" figures and include video, so treat these as rough averages, not a measured per-user rate
- Why so little? Clients send nothing in silence, and channels have few speakers at once. An SFU's egress is speakers × listeners: 10 people, 2 talking, is 2 × 9 = 18 streams
- Today (assumed 3× 2018): ~8 million concurrent voice users → ~650 Gbps of audio; over 25,000+ SFU instances, ~300 users each on average
Conclusion: each call lives on one host, so voice partitions naturally. The hard parts are placing calls well and moving them when a host dies.
Key Metrics Summary#
| Metric | Value |
|---|---|
| Daily active users (Discord, Q4 2025) | 90 million+ |
| Concurrent sessions at peak (assumed) | ~31 million |
| Sessions per Erlang VM (Discord, 2017) | up to 500,000 |
| Largest server (Discord, 2023) | 10 million+ members, 1 million+ online |
| Sessions per relay (Discord, 2023) | up to 15,000 |
| Messages (assumed) | ~1.5 billion a day, ~50,000 a second at peak |
| Dispatches to clients (assumed) | ~6 million a second at peak |
| Voice (Discord, 2018) | 2.6 million+ concurrent, 220+ Gbps |
| SFU instances (Discord, 2026) | 25,000+ |
| Message store (Discord, 2023) | trillions of messages, 72 ScyllaDB nodes |
Requirements and estimation
Q1/3 Discord says 1,000 people online each sending one message costs 1 million notifications. What does 100,000 people online cost?
Answer & explanation
Answer: C
Each message goes to everyone online, and the number of messages grows with everyone online: 100,000 × 100,000.
Q2/3 About 90% of user-guild connections in large servers are passive. What does that buy?
Answer & explanation
Answer: B
Discord reported fan-out became 90% cheaper, and noted a 10× cut in work buys only ~3× in maximum community size, because the work is quadratic.
Q3/3 The 2018 voice fleet sent 220 Gbps to 2.6 million users. About how much per user?
Answer & explanation
Answer: B
220 Gbps ÷ 2.6 million ≈ 85 kbit/s: about one audio stream each, thanks to silence suppression and few concurrent speakers.
Phase 3: High-Level Design (10 minutes)#
Sketch it first: Ishaan's @everyone at 8:55
Ishaan posts "Grand final starts in 5 minutes @everyone" in #announcements. Which component accepts the message and checks he's allowed to ping everyone? Where is it stored before anyone is told about it? Which component knows who in Lobby 91 is online and allowed to see #announcements, and how does the message reach 71,204 screens without one process doing 71,204 sends? What happens for the members who aren't online, and for the thousands who open the channel in the next ten seconds?
One strong answer
- API service takes the HTTP request, rate-limits it, checks the MENTION_EVERYONE permission, and gives the message a Snowflake id (time-sortable).
- Message store, through a data service routed by channel id, writes it to the channel's current partition. Only then is it acknowledged and dispatched.
- The guild process for Lobby 91, one Elixir process holding the server's members, roles, channels and connected sessions, receives the dispatch and fans it out: one send per relay or session node, then permission-checked sends to each active session, which forward it down each gateway websocket.
- Offline members: the mention goes to a push-notification pipeline that buffers and sheds under bursts.
- The readers: thousands of history fetches for the same channel at once are coalesced by the data service into one database query.
"I'll split it into four planes. A connection plane: gateways that hold the websockets and a session process per connected device. A state plane: one process per guild that knows the server's members, roles, channels, voice states and connected sessions, and fans events out. A storage plane: messages, read states and search, behind small services that protect the databases. And a media plane: voice servers that forward audio and video over UDP. Writes go in over HTTP; events come out over the websocket."
System Architecture#
%% title: Discord's four planes: connections, guild state, storage, media
flowchart TD
subgraph L1["Layer 1 · Clients"]
direction LR
APP@{ icon: "sd:desktop", label: "Clients<br/>desktop, phone, web, console" }
end
subgraph L2["Layer 2 · Edge"]
direction LR
GW@{ icon: "tech:websocket", label: "Gateway · Elixir<br/>websockets, zstd-stream" }
API@{ icon: "sd:api", label: "API · Python<br/>REST, rate limits" }
end
subgraph L3["Layer 3 · Real-time state (Elixir)"]
direction LR
SESS["Sessions<br/>one process per device"]:::service
GUILD["Guild processes<br/>one per server, relays"]:::hot
PRES["Presence<br/>status, activities"]:::core
end
subgraph L4["Layer 4 · Voice"]
direction LR
SYNC["Voice syncers<br/>place calls on SFUs"]:::core
SFU@{ icon: "tech:webrtc", label: "SFU fleet<br/>25,000+ instances, UDP" }
end
subgraph L5["Layer 5 · Storage and support"]
direction LR
DS["Data services · Rust<br/>coalesce by channel"]:::service
DB@{ icon: "sd:database", label: "ScyllaDB<br/>messages, 72 nodes" }
ETCD@{ icon: "tech:etcd", label: "etcd<br/>hash ring, discovery" }
PUSH@{ icon: "tech:firebase", label: "Push · FCM<br/>buffer, shed bursts" }
end
APP ==> GW
APP ==> API
APP -.->|"UDP media"| SFU
GW <==> SESS
API ==> DS ==> DB
API ==>|"dispatch, gRPC"| GUILD
GUILD ==>|"fan-out"| SESS
PRES --> SESS
GUILD --> SYNC --> SFU
GUILD -.-> PUSH
ETCD -.-> GUILD
class APP client
class GW,API edge
class SFU external
class DB store
class ETCD store
class PUSH queueKEY INSIGHTS:
- The guild is the unit of everything. One process per server is the single place that knows who is in it, who is online, what each role may see and who is in which voice channel; it orders that server's events and fans them out
- In over HTTP, out over the websocket. A message is stored by the API before anyone is told; the websocket only carries events down, so a dropped connection never loses a message, only the live copy of it
- The media plane is separate. Voice and video never touch the gateway; the guild only passes voice states to the voice control plane (voice syncers, in Discord's 2026 description), which picks the SFU that hosts a call, and clients are told where to connect
Data Flow: Ishaan's Announcement#
%% title: Ishaan's @everyone, from send to 71,204 screens sequenceDiagram autonumber participant I as Ishaan's app participant A as API participant D as Data service participant S as ScyllaDB participant G as Guild process participant R as Relays and sessions participant M as Members' apps I->>A: POST message to #announcements, nonce n7, @everyone A->>A: rate limit, MENTION_EVERYONE permission A->>D: insert, routed by channel id D->>S: write to (channel, bucket), Snowflake id S-->>A: stored A-->>I: 200 with message id A->>G: dispatch MESSAGE_CREATE over gRPC G->>R: one send per relay node R->>R: who can see #announcements, active sessions only R->>M: websocket dispatch with the next sequence number Note over G,M: passive sessions catch up in their next periodic passive update
MEANWHILE, IN PARALLEL:
- The guild hands the @everyone mention to the push pipeline for members who aren't active in the app; the pipeline buffers and, in a catastrophic burst, sheds
- Thousands of members open #announcements; their history requests carry the channel id as a routing key, land on the same data-service instance, and share one ScyllaDB query
High-level design
Q1/3 Why is the message written to the database before the guild fans it out?
Answer & explanation
Answer: B
Store, acknowledge, then dispatch. The websocket is a notification channel, not the record.
Q2/3 What does one guild process hold?
Answer & explanation
Answer: B
It's the in-memory state needed to decide who receives what. History lives in the message store; media in the SFUs.
Q3/3 Ishaan's message carried a nonce. What is it for?
Answer & explanation
Answer: C
The client shows the message immediately and reconciles it with the stored copy when the response or the gateway echo arrives.
Phase 4: Deep Dives (20 minutes)#
Deep Dive 1: One Process per Server, and the Server With a Million Online (Week 1 Concepts: Hot Keys and Skew, Partitioning)#
"Putting each server in one process is what makes Discord simple: one place orders a server's events, so every client sees them in the same order, and one place holds its permissions. It's also a hot key by construction. Most guilds are a few friends and cost nothing. A few are cities. The design has to keep the simple model for the many and split the work for the few, without giving up the ordering."
This builds on Week 1, Day 4 · Hot Keys and Skew and Week 1, Day 1 · Partitioning Deep Dive.
One core, a million sessions
An Elixir process handles one mailbox message at a time, on one core. In 2017, one send took 30 to 70 µs, and one publish from a large guild could take 0.9 to 2.1 seconds. What breaks first as a guild grows? Name three ways to make the guild do less per event without letting clients see a server's events in different orders. And what do you do with a job that takes seconds, like working out who can see an @everyone in a 10-million-member server?
One strong answer
- What breaks: the guild's mailbox. If events arrive faster than one core can fan them out, the queue grows, every action in the server lags, and in the end the node runs out of memory.
- Do less: (1) passive sessions: members who haven't opened the server get periodic summaries, not every event; (2) batch sends by node: send one copy per remote node and let that node deliver locally (Discord's Manifold); (3) relays: move sessions, permission checks and sends to relay processes on other machines, each owning up to 15,000 sessions, so the guild sends once per relay.
- Ordering survives because the guild still decides the order: it sends events to each relay in sequence, and each relay forwards them to its sessions in the order received.
- Slow one-off jobs run in a worker process that reads the member list from shared memory (ETS), so the guild keeps serving while the worker computes who can see the @everyone.
What is published: Discord runs "a single Elixir process per guild as a central routing point for everything happening on that server, and another process (a 'session') for each connected user's client", and keeps one guild process because "some things need a full view", such as who is in a voice channel. In 2017, with servers like /r/Overwatch at up to 30,000 concurrent users, it built Manifold, which sends one copy per remote node and lets that node fan out across its cores. In 2023 it described passive sessions (about 90% of connections in large servers, making fan-out "90% less expensive"), relays that own up to 15,000 sessions each and do "fanout with permission checks", a member list in ETS so worker processes can do slow jobs like working out who can see an @everyone, and Manifold Offload, a separate sender process that only paid off after tuning garbage collection of a heap "gigabytes in size".
%% title: A huge guild: one process orders, relays fan out
flowchart TD
API@{ icon: "sd:api", label: "API<br/>MESSAGE_CREATE via gRPC" }
G["Guild process<br/>order, roles, voice states"]:::hot
ETS@{ icon: "sd:database", label: "ETS member table<br/>shared, read by workers" }
W["Worker process<br/>who can see @everyone"]:::service
R1["Relay 1<br/>15,000 sessions"]:::core
R2["Relay 67<br/>15,000 sessions"]:::core
ACT@{ icon: "sd:users", label: "Active sessions<br/>every event" }
PAS@{ icon: "sd:users", label: "Passive sessions<br/>periodic deltas" }
API ==> G
G -.-> ETS
ETS -.-> W
G ==>|"one send per relay"| R1
G ==> R2
R1 ==>|"permission-checked"| ACT
R2 -.->|"summary"| PAS
class API edge
class ETS store
class ACT,PAS client- Place guilds by consistent hashing. A guild id maps to a node on a hash ring. Discord found the lookup itself was a hot path (500,000 sessions × 5 guilds × 12 µs ≈ 30 seconds after a restart) and compiled the ring into a constant (FastGlobal): about 0.3 µs a lookup.
- Keep small guilds simple. A server of 20 people gets no relays: the guild talks to its sessions directly. Relays start above a size threshold (ours: ~5,000 connected sessions) and are added as sessions arrive, each up to 15,000.
- Give relays only what they need. Copying the whole member list into every relay meant dozens of copies of tens of millions of members, and stalled the guild "for tens of seconds" per new relay; Discord now sends each relay "a tiny percentage of the overall members".
- Move slow work off the loop. Anything that walks all members runs in a worker reading ETS. Discord uses this to hand a guild between machines during deploys, sending "multiple GB" of members while the old process keeps working.
- Quadratic means diminishing returns. A 10× cut in work buys about a 3× bigger server. Expect to keep finding the next bottleneck; budget for it.
# guilds/fanout.py
"""
Fan-out from one guild (illustrative). The guild orders events; relays own sessions,
do the permission checks and the sends. Small guilds talk to sessions directly.
Passive sessions get nothing here: they receive periodic deltas instead.
"""
RELAY_THRESHOLD = 5_000 # connected sessions before a guild starts using relays
RELAY_CAPACITY = 15_000 # Discord: up to 15,000 sessions per relay
class Guild:
def __init__(self, gid, members, roles, channels):
self.gid, self.members, self.roles, self.channels = gid, members, roles, channels
self.seq = 0
self.direct = {} # session_id -> session process (small guilds)
self.relays = [] # relay processes, each with up to RELAY_CAPACITY sessions
def connect(self, session):
if not self.relays and len(self.direct) < RELAY_THRESHOLD:
self.direct[session.id] = session
return
if not self.relays:
self.relays = [spawn_relay(self, list(self.direct.values()))]
self.direct.clear()
relay = min(self.relays, key=lambda r: r.size)
if relay.size >= RELAY_CAPACITY:
relay = spawn_relay(self, []) # only the members it needs
self.relays.append(relay)
relay.add(session)
def dispatch(self, event):
self.seq += 1 # one order for the whole guild
event = {**event, "guild_seq": self.seq}
if self.relays:
for relay in self.relays: # ~67 sends for a million sessions
relay.send(event)
return
for s in self.direct.values():
if s.active and can_see(self, s.user_id, event["channel_id"]):
s.send(event)Deep Dive 2: The Gateway: Millions of Sockets, Fan-Out and Backpressure (Week 3 Concepts: Backpressure and Flow Control; Week 4: Thundering Herd)#
"A websocket that stays open for hours turns the usual web problems around. Requests are cheap; connections are state. The two things that hurt are a client that can't keep up, and a million clients that all come back at the same moment. Both need limits that are designed in, not discovered in an incident."
This builds on Week 3, Day 3 · Backpressure and Flow Control and Week 4, Day 3 · Thundering Herd.
Two kinds of trouble
First: a member's phone, on a train near Lonavala, stops reading its socket while events keep arriving at 30 a second. What happens to his session's memory, and what should? Second: a gateway zone fails and 5 million clients lose their sockets at once. What do they do next, what do they need to avoid downloading everything again, and what stops them stampeding the services behind the gateway?
One strong answer
- A slow consumer grows an unbounded queue, and enough of them take down the node and every healthy session on it. Bound each session's outbound buffer. When it overflows, drop the connection with a "resume" code: the client reconnects and asks for what it missed, or starts over if it was gone too long.
- Resume, don't restart: each dispatch carries a sequence number; the session process outlives the socket for a short window and keeps a replay buffer. The client reconnects to the resume URL with its session id and last sequence number and gets only the missed events, in order.
- Spread the herd: clients wait a random jitter before reconnecting and before their first heartbeat; session starts are admitted through a rate limit and per-node semaphores, so excess callers fail fast and retry later instead of piling up in a queue.
What is published: the gateway is "both the ingress and egress point for all websocket traffic". Clients wait heartbeat_interval * jitter before the first heartbeat (45,000 ms in the docs' example) so clients don't all reconnect "at the exact same time"; every dispatch carries a sequence number, and a client that resumes "will send the missed events in order". Streaming zstandard (2024) cut a MESSAGE_CREATE from 270 to 166 bytes, and delta updates for passive sessions cut them from 35% to 5% of gateway bandwidth: "almost 40%" less in all. Discord notes autoscaling "doesn't work well" for long-lived connections. In 2017, 5 million sessions stampeding ten guild-registry processes caused cascading out-of-memory crashes; its Semaphore library, an atomic ETS counter, now makes callers fail fast. Pushes go through GenStage: pushers demand at most 100 requests, Firebase's limit per connection, and the collector's buffer sheds in "catastrophic situations".
%% title: A connection's life: identify, heartbeat, resume stateDiagram-v2 [*] --> CONNECTING: open websocket CONNECTING --> IDENTIFYING: Hello, heartbeat interval IDENTIFYING --> READY: session started, READY payload READY --> READY: dispatch with sequence n READY --> RESUMING: socket dropped or buffer overflow RESUMING --> READY: missed events replayed, Resumed RESUMING --> IDENTIFYING: session gone, invalid session READY --> CLOSED: user quits CLOSED --> [*] class READY ok class RESUMING queue
- The session is not the socket. A socket can die in a tunnel; the session process (and its replay buffer) lives on for a short window. Discord's gateway monitors each session and, if the session is still alive, "we hook everything back up". Losing a socket costs a resume; losing a session costs a full READY.
- READY is the expensive packet. Discord's example READY was 2.5 MB before compression. Make it lazy (guild stubs first, member lists only for what's on screen), so a reconnect storm doesn't become a bandwidth storm.
- Bound every queue. Each session has an outbound buffer cap (ours: ~5,000 events or ~2 MB); overflow closes the socket with a resumable code. Each guild has a mailbox alarm; when it crosses a threshold, it sheds what can be rebuilt (typing indicators, presence for passive sessions) before what can't (messages).
- Fail fast on overload. A session that can't get a semaphore slot for a busy service errors at once and retries with backoff, rather than joining a queue it will time out of.
- Compress per connection. One streaming context per socket learns that connection's payloads, which are mostly a few hundred bytes; it costs memory per connection, so size it to the node.
- Pushes are allowed to drop; messages aren't. A missed push is a missed buzz; the message is still in the channel. That's why the push pipeline may shed under a burst while the message path never does.
# gateway/session_buffer.py
"""
A session's outbound side (illustrative). Every dispatch gets a sequence number and
is kept in a bounded replay buffer; a slow socket overflows into a resumable close,
not into unbounded memory. Resume replays only what the client missed.
"""
from collections import deque
MAX_PENDING = 5_000 # events queued for a socket that isn't reading
REPLAY_WINDOW = 10_000 # events kept for resume
class Session:
def __init__(self, session_id):
self.id, self.seq = session_id, 0
self.replay = deque(maxlen=REPLAY_WINDOW)
self.pending = deque()
self.socket = None
def dispatch(self, event):
self.seq += 1
frame = {"op": 0, "s": self.seq, "t": event["type"], "d": event["data"]}
self.replay.append(frame)
if self.socket is None:
return # disconnected: replay buffer only
if len(self.pending) >= MAX_PENDING:
self.socket.close(code=4000) # illustrative: Discord's docs list 4000 as
# "Unknown error", reconnect allowed
self.socket = None
self.pending.clear()
return
self.pending.append(frame)
def resume(self, socket, last_seq):
oldest = self.replay[0]["s"] if self.replay else self.seq + 1
if last_seq + 1 < oldest:
return "INVALID_SESSION" # missed too much: identify again
self.socket = socket
for frame in self.replay:
if frame["s"] > last_seq:
self.pending.append(frame) # in order, then a RESUMED marker
self.pending.append({"op": 0, "t": "RESUMED", "s": self.seq, "d": {}})
return "RESUMED"Guilds and the gateway
Q1/3 Why not split one huge server across several guild processes, each owning some members?
Answer & explanation
Answer: B
Discord kept one guild process as the central routing point and moved the fan-out work to relays instead.
Q2/3 What does a relay do?
Answer & explanation
Answer: C
Relays split one guild's fan-out across machines while the guild keeps ordering.
Q3/3 A client's socket stops reading and its queue grows. What's the right response?
Answer & explanation
Answer: C
Bounded buffers protect the node; sequence numbers and the replay buffer make the drop recoverable.
Deep Dive 3: Presence and the Member List (Week 5 Concepts: Consistency Models)#
"Presence is the most frequent event and the least important one. Nobody is harmed if the green dot next to a name is three seconds late. Everybody is harmed if presence updates crowd out messages. So I'd make presence explicitly weaker: eventually consistent, coalesced, and sent only to people who are looking."
This builds on Week 5, Day 1 · Consistency Models and Week 1, Day 5 · Session Store Design.
Online — 71,204
Ishaan is on desktop and on his phone. His desktop says "Playing BGMI"; his phone has been idle for ten minutes. What status should the other 71,203 people see for him? When he closes the laptop, how does anyone find out, if the laptop never says goodbye? Lobby 91's member list has four lakh entries sorted by role, then status, then name, and each client shows about a hundred rows. When someone in the list goes idle, who needs to hear about it? And the header says "Online — 71,204": must that number be exact?
One strong answer
- Combine devices. Presence belongs to the user, built from each session's status: the most "present" one wins (online beats idle beats offline), and activities come from the session that reports them. Ishaan shows online, playing BGMI; per-device status is kept for clients that show it.
- Offline is detected, not announced. Every session process is monitored; when it dies, or misses heartbeats long enough to be closed, the monitors fire and presence recomputes from the sessions that are left. No goodbye message is needed.
- Send to who's looking. Each client subscribes to the ranges of the member list it has on screen (rows 0–99, say). A change goes to the sessions whose ranges include that member's old or new position, as an insert, update or delete at an index. Everyone else's list is wrong only in places they aren't looking.
- The count can lag. "Online — 71,204" is recomputed and sent every few seconds. Nobody can check it, and an exact count would mean an event per change to every client.
What is published: in 2019 Discord described sending member-list updates only for "the visible portion", which needed a sorted set reporting the index of every change; pure Elixir topped out around 250,000 members, so it wrote SortedSet in Rust (0.61 to 3.68 µs per insert at up to 1,000,000 items), which "powers every single Discord guild". The gateway docs show the same idea for apps: above large_threshold (50 to 250) members, offline members are left out, and for guilds over 75,000 members only members in voice are sent until requested. When a session exits, process monitors tell its guilds to "stop trying to send you messages, presence updates, and everything else". A 2026 post describes a presence service "which fans out DMs and status updates to users".
%% title: One idle event, delivered only where it's visible
flowchart TD
S1@{ icon: "sd:desktop", label: "Desktop session<br/>online, Playing BGMI" }
S2@{ icon: "sd:phone", label: "Phone session<br/>idle 10 min" }
P["Presence<br/>combine sessions, coalesce"]:::core
G["Guild process<br/>member list, SortedSet"]:::hot
F@{ icon: "sd:users", label: "Friends<br/>status change" }
V@{ icon: "sd:users", label: "Sessions viewing rows 0-99<br/>update at index 37" }
N@{ icon: "sd:users", label: "Everyone else<br/>nothing now, count later" }
S1 --> P
S2 --> P
P ==> G
P --> F
G ==> V
G -.-> N
class S1,S2 client
class F,V,N client- Session status, user presence. Store status per session; derive the user's presence from all live sessions. One device going idle changes nothing visible if another is active, so many changes stop at this step.
- Coalesce. If a user goes online, idle and online again within a few seconds (a phone switching networks), send the last state only. A presence change can wait a second or two; a message can't.
- Member list as ranges. The guild keeps the sorted member list (role, status, name) and, per session, the ranges it watches. A change becomes an operation at an index, sent only to sessions watching that index. Very large guilds can show only online members, so offline members never enter the list at all.
- Consistency by kind. Messages: per-channel order, read-your-writes for the sender. Presence and counts: eventual, seconds. Permissions: strong, because a role removed from a member must stop the next message reaching them, so permission changes go through the guild's ordered loop like messages.
- Read states are their own service. Unread counts and mentions are one record per user per channel, "billions" of them; Discord keeps them in LRU caches committed to Cassandra, and rewrote the service from Go to Rust in 2020 to remove garbage-collection spikes.
# presence/aggregate.py
"""
User presence from session statuses, and member-list updates only for watchers
(illustrative). Offline comes from a session exiting (a process monitor firing),
never from the client announcing it.
"""
RANK = {"online": 3, "dnd": 3, "idle": 2}
def user_presence(sessions: dict) -> dict:
"""sessions: session_id -> {"status": ..., "activities": [...], "since": t}"""
live = [s for s in sessions.values() if s["status"] != "offline"]
if not live or any(s["status"] == "invisible" for s in live):
return {"status": "offline", "activities": []} # invisible looks offline to others
best = max(live, key=lambda s: (RANK[s["status"]], s["since"]))
return {"status": best["status"], "activities": [a for s in live for a in s["activities"]]}
def on_member_change(member_list, watchers, user_id, new_presence):
"""member_list: SortedSet-like with index_of/move; watchers: session -> [(lo, hi)]"""
old_i = member_list.index_of(user_id)
new_i = member_list.update(user_id, new_presence) # re-sorts by role, status, name
ops = [("UPDATE", new_i)] if old_i == new_i else [("DELETE", old_i), ("INSERT", new_i)]
for session, ranges in watchers.items():
touched = [(op, i) for op, i in ops if any(lo <= i <= hi for lo, hi in ranges)]
if touched:
# "MEMBER_LIST_UPDATE" is our illustrative event name; it is not in Discord's public docs
session.send({"t": "MEMBER_LIST_UPDATE", "ops": touched, "presence": new_presence})Deep Dive 4: Trillions of Messages: Hot Partitions and a Days-Not-Months Migration (Week 1 Concepts: Partitioning, Hot Keys; Week 4: Thundering Herd)#
"Messages are append-mostly and read by channel, newest first, so the shape is obvious: partition by channel, sort by time. The interesting part is what Discord learned living with that shape for seven years: partitions that never stop growing, deletes that aren't free, and one busy channel slowing everyone else on its nodes."
This builds on Week 1, Day 1 · Partitioning Deep Dive, Week 1, Day 4 · Hot Keys and Skew and Week 4, Day 3 · Thundering Herd. The migration itself is the war story Discord moved trillions of messages to a new database in days, not months.
Twenty thousand people open #announcements
Lobby 91's #announcements has existed for four years. Messages are keyed by channel and time. What goes wrong if a channel's messages all live in one partition forever? At 8:55 pm, 20,000 clients open the channel within ten seconds, all asking for the latest 50 messages. What does that do to the three replicas holding that partition, and to every other channel stored on them? And if a moderator bulk-deletes 200,000 spam messages from #live-chat, why might the next person to open #live-chat wait twenty seconds?
One strong answer
- Unbounded partitions: a channel that lives for years grows a partition without limit; compaction, repair and moving it between nodes get slower, and it can't be spread over the cluster. Add a time bucket to the partition key, so each partition holds a fixed window of one channel.
- The stampede: 20,000 identical reads hit the same partition's replicas; with quorum reads, those nodes slow down, and so does every other query routed to them. Put a service in front that coalesces identical concurrent reads into one query, and routes all requests for a channel to the same instance so coalescing works.
- Tombstones: in Cassandra-style stores a delete is a write (a tombstone), kept until compaction after a grace period. A read for "the latest 50" must scan past 200,000 tombstones before it finds a live message. Shorten the grace period where repairs run often, remember which buckets are empty, and never write nulls you don't need.
What is published: Discord moved from MongoDB to Cassandra in 2015–2016 with the key ((channel_id, bucket), message_id): a Snowflake id (time-sortable) and a bucket of about 10 days to keep partitions under 100 MB. A channel where millions of messages had been deleted took 20 seconds to load because of tombstones; Discord cut their lifetime from 10 days to 2 and stopped writing nulls, which had created 12 tombstones per message. By 2022, 177 Cassandra nodes held trillions of messages, and hot partitions raised latency "across our entire database cluster". Discord put Rust data services in front, "roughly one gRPC endpoint per database query" whose "big feature … is request coalescing", routed by channel id; then dual-wrote to ScyllaDB, copied history with a Rust migrator at up to 3.2 million messages a second (its estimate was nine days; "several days later" it reached 99.9999% and stalled on tombstones until they were compacted away), compared a sample of reads, and switched in May 2022. Result: 72 nodes instead of 177, p99 reads from 40–125 ms to 15 ms, inserts from 5–70 ms to 5 ms.
%% title: Reading the latest messages: route, coalesce, then one query
flowchart TD
C@{ icon: "sd:users", label: "20,000 clients<br/>latest 50 in #announcements" }
API@{ icon: "sd:api", label: "API<br/>routing key = channel id" }
DS["Data service instance<br/>one per channel by hash"]:::hot
T["In-flight task<br/>channel 81, bucket 412"]:::core
DB@{ icon: "sd:database", label: "ScyllaDB<br/>(channel, bucket) partition" }
C ==> API
API ==>|"consistent hash"| DS
DS ==>|"first request starts it"| T
DS -.->|"19,999 subscribe"| T
T ==>|"one query"| DB
class C client
class API edge
class DB store- Snowflake ids are the clustering key. They're unique and sort by time, so "messages before id X" is a range scan, and a message id tells you its bucket without a lookup. Discord's ids carry 42 bits of milliseconds since the first second of 2015.
- Buckets bound partitions. Bucket = (timestamp since the epoch) ÷ ~10 days. To load recent messages, scan buckets from now backwards until you have 50; quiet channels touch a few, busy channels one.
- Coalescing beats caching here. A cache needs invalidation on every edit, delete and reaction; coalescing just shares the result of a query already in flight, so it can't serve anything stale beyond that one query. Discord said this cut traffic spikes like an @everyone announcement "significantly".
- Route by the thing that's hot. Coalescing only works if identical requests meet on the same instance, so the routing key is the channel id; a hot channel concentrates on one data-service instance, which is cheap, instead of on the database, which isn't.
- Migrate in steps, newest first. Move the smaller databases first and learn the new one in production; put the protecting layer in front; dual-write new data; copy history behind it with checkpoints (Discord's migrator checkpointed token ranges locally in SQLite); compare a sample of reads; then flip. Each step is reversible until the last.
- Search shards by guild too, and the biggest guilds hit Lucene's ~2 billion document limit; Discord now gives them a dedicated Elasticsearch cell with multi-shard indices (Discord Engineering, April 2025).
# data/messages.py
"""
Message reads behind a data service (illustrative): Snowflake ids give the bucket,
requests for the same channel are routed to one instance, and identical in-flight
queries are coalesced so a stampede costs one database read.
"""
import asyncio
DISCORD_EPOCH_MS = 1_420_070_400_000 # first second of 2015
BUCKET_MS = 10 * 24 * 3600 * 1000 # ~10 days per partition
def bucket_of(snowflake: int) -> int:
return (snowflake >> 22) // BUCKET_MS # top 42 bits: ms since the Discord epoch
class Coalescer:
def __init__(self, db):
self.db, self.inflight = db, {}
async def latest(self, channel_id: int, before: int, limit: int = 50):
key = (channel_id, before, limit)
if key in self.inflight:
return await self.inflight[key] # share the query already running
task = asyncio.ensure_future(self._read(channel_id, before, limit))
self.inflight[key] = task
try:
return await task
finally:
self.inflight.pop(key, None)
async def _read(self, channel_id, before, limit):
out, b = [], bucket_of(before)
floor = bucket_of(channel_id) # a channel id is a Snowflake too
while len(out) < limit and b >= floor:
if not await self.db.bucket_known_empty(channel_id, b):
out += await self.db.select(channel_id, b, before=before, limit=limit - len(out))
b -= 1
return outPresence and storage
Q1/3 Ishaan's desktop is online and his phone is idle. What do others see?
Answer & explanation
Answer: B
Status is per session; presence is derived per user.
Q2/3 A member list has 400,000 entries and a client shows 100 rows. Who gets a presence change for row 37?
Answer & explanation
Answer: C
Lazy member lists: send index operations only to sessions that can see them.
Q3/3 20,000 clients ask for the same latest 50 messages at once. What protects the database?
Answer & explanation
Answer: B
Request coalescing plus consistent-hash routing, the layer Discord added before its ScyllaDB migration.
Deep Dive 5: Voice Channels You Can Drop Into (Week 10 Concepts: Capacity Planning; Week 9: Security Architecture)#
"A meeting is scheduled, has a host and ends. A voice channel is a room that's always there; people wander in and out all night, and nobody waits for anyone to start it. So the system design question isn't 'how do I run a call', which the Zoom problem covers, but 'how do I make joining feel instant when a call can come into existence, change members every few seconds, and lose its server, all without anyone pressing anything'."
This builds on Week 10, Day 4 · Capacity Planning and Week 9, Day 5 · Security Architecture. For how an SFU forwards rather than mixes, and simulcast, see Bonus Problem 23 (Zoom).
Ishaan clicks "Team Lobby 3"
Four players from Hyderabad are already talking in Team Lobby 3. Ishaan, in Pune, clicks it. What has to happen before he hears them, and which parts were already done before he clicked? The call's voice server dies mid-conversation: who notices first, and how do five people end up on a new server without anyone clicking again? Someone starts a DDoS against that voice server's IP: why can't they find the players' own IPs? And since March 2026 the call is end-to-end encrypted: what has to change for the group every time someone drops in or out?
One strong answer
- Before the click: the channel already has a voice server, because the first person to join triggered placement; the guild already knows the four voice states. Ishaan's client sends a voice-state update over the gateway it already has; the guild records it, tells the voice control plane, and replies with the voice server's address and a token.
- The join: the client opens a voice websocket to that server, gets a stream id (SSRC), its UDP address and the encryption modes, discovers its own public address, picks a mode, and starts sending UDP. "Voice Connected" means UDP messages went both ways.
- Failover: the client notices the voice websocket drop; the server stops passing health checks and is removed from discovery. The control plane picks a new server, pushes all the voice states to it, and every client is told to reconnect there.
- Privacy: everyone sends media to the SFU, never to each other, so no participant learns another's IP. A DDoS'd voice server is treated like a dead one: removed, and its calls moved.
- E2EE: each join or leave changes the group's key epoch (an MLS group), so a new member can't decrypt what came before and a departed one can't decrypt what comes after.
What is published: in 2018 a voice server was a signalling component plus a home-grown C++ SFU; servers reported load into etcd, and the guild service picked "the least utilized voice server in the given region" for the whole guild: all of a guild's voice channels shared one voice server. Clients show "Awaiting Endpoint" while one is chosen and "Voice Connected" once UDP works. A crashed SFU is restarted and rebuilt "without any client interaction"; a dead or DDoS'd voice server is removed from discovery and its calls reassigned. In 2026 Discord described voice syncers that "decide where to place and manage every active call" over "more than 25,000 external" SFU instances, with one syncer per guild and child syncers per voice channel; "we pick one SFU instance to host an entire call"; a newer Rust-based SFU; and 80%+ of traffic on Cloudflare's edge. DAVE encrypts each frame with per-sender keys from an MLS group that the SFU can't read; since early March 2026 it covers every call except stage channels.
%% title: Joining a voice channel: control over the gateway, media over UDP sequenceDiagram autonumber participant C as Ishaan's client participant GW as Gateway participant G as Guild process participant VS as Voice syncer participant V as Voice server C->>GW: voice state: join Team Lobby 3 GW->>G: voice state update G->>VS: channel has 5 voice states Note over VS: call already placed on a host near its members VS-->>G: endpoint and token G-->>C: voice server update, endpoint and token C->>V: voice websocket identify, token, max DAVE version V-->>C: ready, SSRC, UDP address, encryption modes C->>V: UDP IP discovery, then select protocol V-->>C: session description, mode, DAVE version C->>V: MLS key package V-->>C: MLS welcome, new epoch Note over C,V: Voice Connected · encrypted Opus frames flow
- The room exists before you enter. A voice channel with people in it already has a host; joining is a voice-state write plus a websocket and a UDP handshake to a known address. Placement happens once, when the first person joins, so "Awaiting Endpoint" should be rare and short.
- Place by where people are, not where the first person is. In Discord's first Cloudflare test, in Iceland in February 2025, a call started by an Icelander with friends in Germany could land in Reykjavik, and ping for non-Icelanders on Iceland-hosted calls "jumped 2.7x". A call has one host; choose it from the participants' regions (and re-evaluate when the mix changes), not from the first joiner alone.
- Silence is free. No speech, no packets; the SFU forwards each speaker's stream to each listener, so a big channel with one talker costs one stream per listener. That's why Discord's 2018 totals work out to only ~85 kbit/s per voice user (our arithmetic: 220 Gbps ÷ 2.6 million), and why a channel where 1,000 people take turns speaking is affordable.
- Moderation lives in the SFU. Server mute drops a user's packets at the SFU; a moderator "moving" someone is a voice-state change the guild orders. With E2EE, the SFU still forwards and drops packets, but can't read them.
- The control plane is the fragile part. SFUs forward packets independently; what can collapse is the service that creates calls and talks to thousands of SFUs. Rate-limit call creation per guild and per channel, and partition the control plane so one overloaded process can't block the rest (see the Final Challenge).
- Capacity is per host and per region. Four SFU workers sharing one NIC queue lost 1.5 to 2% of packets at peak, so Discord halved density until the platform was fixed. Plan voice capacity with packet loss, not CPU, as the limit.
# voice/placement.py
"""
Choosing a host for a call (illustrative). One SFU hosts each call; pick the region
that minimises the participants' worst latency, then the least-loaded healthy host
there. Moving a call reconnects everyone, so re-place only for a large gain.
"""
def choose_region(participants, rtt_ms) -> str:
"""participants: list of client regions; rtt_ms[(client_region, host_region)] -> ms"""
hosts = {h for (_, h) in rtt_ms}
return min(hosts, key=lambda h: (max(rtt_ms[(p, h)] for p in participants),
sum(rtt_ms[(p, h)] for p in participants)))
def choose_host(discovery, region, exclude=()):
healthy = [h for h in discovery.hosts(region)
if h.healthy and not h.under_attack and h.id not in exclude]
if not healthy:
raise NoCapacity(region)
return min(healthy, key=lambda h: h.load) # least utilised, as in 2018
Deep Dive 6: Rate Limits, Spam and Raids (Week 1 Concepts: Rate Limiting at Scale; Week 9: Noisy Neighbours)#
"In a community platform the worst traffic isn't the most traffic, it's the most expensive: one @everyone in a server of millions, one bot that loops on errors, two hundred fresh accounts posting the same link. Rate limits here protect three things at once: the platform's machines, each server's members from each other, and other customers from a noisy neighbour."
This builds on Week 1, Day 3 · Rate Limiting at Scale and Week 9, Day 2 · Noisy Neighbour.
8:57 pm in #live-chat
Two minutes after Ishaan's announcement, 200 accounts created in the last hour join Lobby 91 and each posts "free UC" with the same link, one message a second. At the same time, a tournament bot Ishaan's team wrote is retrying a failed request in a tight loop, getting 429s back. Which limits should catch each of these, and keyed on what? What should the bot see so it can behave? What tools does Ishaan need as a moderator that no platform-wide limit replaces?
One strong answer
- The raid: per-user limits won't catch 200 users at one message a second each. Catch it with per-channel slowmode (one message per user per N seconds), content rules that block links widely reported as spam before they're posted, a join-rate alarm per server, and a way to pause invites without deleting them.
- The bot: a per-route bucket keyed by the route and its top-level resource (channel, guild or webhook id), a global per-bot limit, and a limit on invalid requests (401, 403, 429) per IP, which ends in a temporary ban if the bot keeps hammering.
- What it sees: headers with the limit, what's left, when it resets and which bucket it hit; a 429 with
retry_after; and whether the limit was its own, global, or shared by the resource. - Moderator tools: slowmode, AutoMod rules, pausing invites, mass bans and raid reports. Limits keep the platform up; moderators keep the community usable.
What is published: Discord's API has per-route limits that "account for top-level resources" (channel, guild or webhook ids); a global limit of 50 requests a second per bot; an invalid-request limit of 10,000 per 10 minutes per IP, after which the IP is temporarily banned; and X-RateLimit-* headers including a Scope of user, global or shared, with shared 429s "not counted against you". On the gateway, an app may send 120 events per 60 seconds per connection and IDENTIFY at most 1,000 times in 24 hours (more for very large bots). Slowmode runs from 0 to 21,600 seconds and doesn't apply to bots or to users with the bypass-slowmode permission. In September 2022 Discord said AutoMod had removed "over 20 million unwanted messages" since June, and added spam-content and mention-spam blocking, paused invites and raid reports. Internally, the gateway has a start-session rate limit for backpressure, and a 2025 project added automatic circuit breaking to shed demand in the API when it is overloaded (Discord Engineering, April 2026).
%% title: One message, five checks before it is fanned out
flowchart TD
R@{ icon: "sd:api", label: "POST message<br/>bot token, channel 81" }
GL{"Global per-bot limit<br/>50 a second"}
RT{"Route bucket<br/>route + channel 81"}
SM{"Slowmode<br/>users only, bots exempt"}
AM{"AutoMod<br/>spam link, mention spam"}
OK["Store and dispatch"]:::ok
NO["429 with retry_after<br/>bucket and scope"]:::bad
BLK["Blocked before posting<br/>moderators alerted"]:::bad
R ==> GL
GL ==>|"under"| RT
GL -.->|"over"| NO
RT ==>|"under"| SM
RT -.->|"over"| NO
SM ==>|"allowed"| AM
SM -.->|"too soon"| NO
AM ==>|"clean"| OK
AM -.->|"match"| BLK
class R edge- Key limits by the resource that costs. A route bucket per channel means a bot busy in one server can't exhaust its quota in another, and one channel's spam can't slow other channels. The cost of a message is set by where it goes, so the expensive cases get their own limits: @everyone needs a permission, and AutoMod can block messages that mention too many members.
- Return the limits, don't make clients guess. Remaining and reset headers let a well-behaved client slow down before it's refused. Scope tells it whether the limit was its own (back off), global (back off everything) or shared (someone else filled the resource's bucket: wait, it isn't counted).
- Punish loops, not bursts. The invalid-request limit targets clients that keep sending requests they know will fail. A bot that hits 429 once and waits is fine; one that retries in a loop is cut off at the edge before it reaches any service.
- Limit work, not just requests. The worst floods are internal: session starts after a mass disconnect, calls being recreated. Each gets its own limit, re-tuned after every infrastructure change (see the Final Challenge).
- Fan-out is a noisy-neighbour problem. One busy server shares nodes with thousands of small ones; relays, mailbox alarms and shedding typing and presence first keep its tournament night from becoming everyone's lag.
# ratelimit/buckets.py
"""
Per-route, per-resource token buckets with Discord-style response headers
(illustrative). The bucket key includes the top-level resource (channel, guild or
webhook id), so one channel's limit never blocks another.
"""
import time
import re
TOP_LEVEL = re.compile(r"/(channels|guilds|webhooks)/(\d+)")
class Bucket:
def __init__(self, limit: int, per_s: float):
self.limit, self.per_s = limit, per_s
self.tokens, self.reset_at = limit, time.time() + per_s
def take(self, now: float):
if now >= self.reset_at:
self.tokens, self.reset_at = self.limit, now + self.per_s
if self.tokens == 0:
return False
self.tokens -= 1
return True
def check(store, principal: str, method: str, route: str, path: str, policy: dict):
now = time.time()
if not store.bucket(f"global:{principal}", 50, 1.0).take(now):
return 429, {"X-RateLimit-Global": "true", "X-RateLimit-Scope": "global"}
m = TOP_LEVEL.search(path)
resource = m.group(2) if m else "-"
limit, per_s = policy[(method, route)]
b = store.bucket(f"{principal}:{method}:{route}:{resource}", limit, per_s)
ok = b.take(now)
headers = {"X-RateLimit-Limit": str(limit), "X-RateLimit-Remaining": str(b.tokens),
"X-RateLimit-Bucket": f"{method}:{route}",
"X-RateLimit-Reset-After": f"{b.reset_at - now:.3f}"}
if not ok:
store.count_invalid(principal) # 10,000 in 10 min → edge ban
return 429, {**headers, "X-RateLimit-Scope": "user", "Retry-After": f"{b.reset_at - now:.3f}"}
return 200, headersVoice and rate limits
Q1/4 Ishaan clicks a voice channel where four people are already talking. What usually doesn't have to happen?
Answer & explanation
Answer: C
Placement happens once per call; joining is a state write and two handshakes to a known host.
Q2/4 A voice server is under DDoS. What did Discord describe doing in 2018?
Answer & explanation
Answer: B
Clients only ever know the voice server's address, so the attack can't follow them to each other; the calls move (2018 post).
Q3/4 In Discord's 2025 Iceland test, why did some calls get worse for users in Germany?
Answer & explanation
Answer: B
Discord: "host placement mattered more than coverage". Our design places a call by its participants' regions, not by who started it.
Q4/4 A bot gets a 429 with X-RateLimit-Scope: shared. What should it do?
Answer & explanation
Answer: C
Shared-scope limits belong to the resource, not to the caller.
Phase 5: Scaling and Edge Cases (5 minutes)#
"Walk me through Lobby 91's final from the platform's side, then the cases that bite."
Ishaan's Tournament Night#
- 8:00 pm · The server fills
- Online climbs from 20,000 to 60,000; Lobby 91's guild crosses the relay threshold early and runs on five relays; most members' sessions are passive until they click in
- 8:30 pm · Voice channels fill
- Sixteen team channels and the stage get voice states; each call is placed once, on a host chosen from its participants' regions
- 8:55 pm · @everyone
- One message stored, dispatched once per relay; a worker works out the mention's audience; ~20,000 history fetches coalesce into a handful of queries
- 8:57 pm · The raid
- AutoMod blocks the reported link before it posts; slowmode goes to 10 s in #live-chat; invites are paused; the raiders' accounts are banned
- 9:00 pm to 11:30 pm · The final
- Ishaan drops into Team Lobby 3 in under a second; the casters broadcast from the stage; #live-chat peaks at each knock-out
Edge Cases#
Edge case 1: @everyone in a server with 10 million members
Problem: Working out who can see the message means walking every member's roles against the channel's overrides, which takes seconds; the guild can't stop for that, and millions of phones may buzz.
Solution: The guild dispatches the message to online active sessions at once; a worker process computes the mention's audience from the ETS member table, off the guild's loop; mention notifications go through the push pipeline with demand-driven backpressure and a bounded buffer that sheds in a catastrophe. Servers can turn @everyone off for most roles.
Edge case 2: Deploying the node that hosts a huge guild
Problem: Handing a guild to another machine means moving multiple gigabytes of member state and reconnecting a million sessions, and the guild stalls while it does.
Solution: Spawn the new process, stream the members to it from a worker while the old process keeps working, then switch sessions over and replay the short backlog. Never terminate a host until its entity count reaches zero.
Edge case 3: A voice server dies, or is attacked
Problem: Every call on it goes silent.
Solution: The host leaves discovery; clients ask for a new endpoint; the control plane re-places each call and pushes its voice states (Deep Dive 5). Rate-limit re-placement so a dead region doesn't become a stampede on the next.
Edge case 4: A raid is cleaned up with a bulk delete
Problem: 200,000 tombstones sit in #live-chat's latest buckets, and the next reader scans past all of them.
Solution: Short grace periods where repairs run nightly, skip known-empty buckets, compact the affected ranges (Deep Dive 4).
Scaling and edge cases
Q1/2 Who works out the audience of an @everyone in a 10-million-member server?
Answer & explanation
Answer: B
Slow, one-off jobs that walk all members move off the guild's single-threaded loop.
Q2/2 When may a host running guild processes be terminated in a deploy?
Answer & explanation
Answer: B
Graceful hand-off is the invariant; a timer is not.
Phase 6: Monitoring and Operations#
"Four questions on a busy night: are sessions connected and stable, is any guild falling behind, are voice calls being placed and sounding good, and are the limits holding? Then one more: when something is slow, can I see where?"
What is published: Discord's "guild timings" record "how much of the current minute has been spent on each action type". In 2026 it added tracing from the API's dispatch (1.69 ms in its example) through the guild's fan-out (357 µs) into each session, where each session keeps a sampled trace's spans 100% of the time for a single-recipient message, 10% at 100 recipients and 0.1% at 10,000+ (on top of head sampling), so a million-session fan-out doesn't create a million spans. Voice is measured by client-reported ping, packet loss and "expand ratio (the measurement of audio frames we stretch to cover gaps)". After its March 2026 outage, Discord added monitoring of its HTTP connection-pool mailboxes and is expanding monitoring of its most critical processes.
Monitoring Dashboard#
Connections
| Metric | Now |
|---|---|
| Sessions connected | 30.4M |
| IDENTIFY / RESUME per second | 6,100 / 9,800 |
| Sessions closed for buffer overflow | 210 /s |
| Gateway egress (compressed) | 48 Gbps |
Guilds
- Largest guild mailbox: 1,940 messages (alarm at 50,000); guilds on relays: 2,310
- Top guild by time: presence updates 41% of its minute
- Fan-out trace, @everyone in Lobby 91: API 1.8 ms → guild 0.4 ms → p99 session 120 ms
Voice and storage
- "Awaiting Endpoint" p95: 180 ms; packet loss p50: 0.3%; expand ratio: 1.1%
- History read p99: 14 ms; coalesced share of reads: 38%
Limits
- 429s: 52,000/s (shared scope 6%); IPs under invalid-request restriction: 1,240
SLOs for the Platform#
SLO 1: Messages arrive
- Target: 99.9% of messages delivered to connected active sessions within 500 ms (2 s in guilds with relays); zero acknowledged messages lost
SLO 2: Connections recover
- Target: 99% of dropped sockets resumed without a new READY; IDENTIFY p99 under 3 s outside incidents
SLO 3: Voice connects and sounds right
- Target: voice join p95 under 1 s; fewer than 1% of calls with packet loss above 2% in any 5-minute window
SLO 4: History is fast
- Target: p99 latest-messages read under 50 ms; no partition over 100 MB
Reading the dashboard
Q1/3 One guild's mailbox climbs from 2,000 to 400,000 over five minutes. What do you check first?
Answer & explanation
Answer: B
A growing mailbox means the guild's one core can't keep up; per-type timings show why.
Q2/3 Why sample traces by the number of recipients?
Answer & explanation
Answer: A
Within a sampled trace, Discord's sessions keep their spans 100% of the time for one recipient, 10% for 100, 0.1% for 10,000+.
Q3/3 RESUMEs per second jump tenfold while IDENTIFYs stay flat. What happened?
Answer & explanation
Answer: B
Resumes mean sessions are alive. A jump in IDENTIFYs would mean sessions were lost, which is far more expensive.
Final Challenge: Seventeen Percent of Sessions, Gone at Once#
The interviewer leans back. "This one's from Discord's own postmortem. Tell me what you'd have done, and what you'd change."
"On 25 March 2026, a routine change to make session pods bigger and fewer was deployed to one zone. Kubernetes terminated half that zone's pods, and the hand-off of their session processes didn't start before the termination grace period ran out. 17% of all sessions stopped at once. Clients reconnected, gateways in that zone ran out of memory, and voice and video were degraded for over three hours. Walk me through why voice broke, and fix it."
- 12:13 PDT: a session-pod resize starts in one zone; 17% of sessions across Discord stop ungracefully
- Gateways: the per-host session-start rate limit hadn't been re-tuned after the migration from VMs to Kubernetes, which ran more pods than there had been VMs, so more starts got through in total; that zone's gateways ran out of memory, and their users failed over to two other zones
- Voice: emptied calls were stopped, then recreated as users came back; 15 voice syncer instances opened a flood of HTTPS connections to 25,000+ SFUs through two single-process supervisors; their mailboxes grew; etcd registrations (60 s TTL) expired and instances fell out of the hash ring
- Users: mostly unable to start or join calls, stuck at "Awaiting Endpoint", until 15:30 PDT
What do you do at 12:30, and what do you change by next week?
Decide how to get voice back without making the herd worse, then what to change in deploys, in rate limits, in the voice control plane and in monitoring so that losing a zone's sessions is a blip.
One strong answer
- Reduce demand first. Restarting every syncer at once re-creates millions of calls in one burst; Discord tried it at 13:05, and by 13:09 the mailboxes were growing again. Lower the spawn limits for syncers in every service that owns calls (guilds, DM calls, streams), including the per-channel child syncers that the guild limit didn't cover. Discord had low limits in the calls, streams and guilds services by 13:43.
- Then add supply. Double the syncer cluster so each instance carries half the calls; Discord brought 15 new instances online by 14:15 and was healthy by 14:26. Restart unhealthy instances one by one once demand is limited.
- Deploys: never scale down a stateful pod until its entity count is zero. Discord added an admission webhook that rejects a scale-down until the pod has handed off its processes.
- Rate limits: a per-host limit changes meaning when the host count changes. Re-tune every per-host limit as part of any migration; add limits on endpoint selection and connection creation in the voice control plane.
- Remove the single-process bottleneck. Replace the one connection-pool supervisor with a partitioned set, and keep connection lifecycles out of the shared supervisor, so outbound connections and the etcd heartbeat can't queue behind a flood. Discord did both; it also plans to spread the voice control plane across regions.
- The rule: a large spike will find a bottleneck. When it does, ask whether you can increase supply or reduce demand, then do the cheaper one first.
Final challenge · Quest check
Q1/3 Why did restarting the whole voice syncer cluster at 13:05 not fix voice?
Answer & explanation
Answer: B
A full restart without lower spawn limits re-creates the flood that caused the problem.
Q2/3 Why was the guild's syncer rate limit "especially ineffective"?
Answer & explanation
Answer: B
Limit the unit that does the expensive work, here each call's syncer.
Q3/3 Why did instances drop out of the hash ring?
Answer & explanation
Answer: B
A health heartbeat must never queue behind the traffic it is supposed to report on.
Interview Conclusion#
"Why not put a message broker in the middle: a topic per channel, every client subscribes?"
"Because the hard part isn't moving bytes, it's deciding who may receive each one and in what order. A guild process holds the roles, overrides and connected sessions, applies a permission change and a message in one order, and knows which sessions are active or passive. A broker topic per channel would need the subscriptions rewritten every time a role changes, couldn't check permissions per recipient, and couldn't give one order across a server's channels and voice states. The guild process is the subscription manager; relays are its broker."
"And why not peer-to-peer voice? It's cheaper."
"Three reasons Discord gave in 2018: a group call with many people becomes too expensive for every client to send to every other; routing through Discord's servers means 'your IP address is never leaked', so nobody can DDoS you; and moderation, because a server mute is the SFU dropping packets. End-to-end encryption now means the SFU forwards what it can't read."
"The single most important decision?"
"One ordered process per server, with the expensive part, fan-out, pushed outwards: to relays, to passive summaries, to the ranges a client can see. That keeps Discord simple for the millions of small servers and survivable for the one with a million people online."
Summary: Concepts Applied from 10-Week Course#
- Week 1: Data at scale
- Partitioning — guilds on a consistent-hash ring; messages by (channel, 10-day bucket)
- Rate limiting — per-route, per-resource buckets; global and invalid-request limits; slowmode
- Hot keys and skew — huge guilds on relays; hot channels behind coalescing data services
- Session store — a session per device that outlives its socket for resume
- Week 3: Messaging and async processing
- Backpressure — bounded session buffers, semaphores, GenStage demand and shedding for pushes
- Week 4: Caching
- Thundering herd — jittered heartbeats and reconnects; request coalescing for history reads
- Week 5: Consistency and coordination
- Consistency models — ordered messages and permissions per guild; eventual presence and counts
- Week 9: Multi-tenancy and security
- Noisy neighbours — per-guild limits and shedding; IP hiding and end-to-end encrypted media
- Week 10: Production readiness
- Capacity planning — voice hosts limited by packet loss, not CPU; observability with recipient-scaled sampling; incident response by supply and demand
Why Discord Matters#
The work grows with the square of the room
- In a community, everyone talks and everyone listens, so a server ten times bigger costs a hundred times more. Every technique here (passive sessions, relays, lazy member lists) is a way of not sending something
Live state is the product
- The online count, the member list, the avatars in a voice channel: Discord's screen is a live view of thousands of people's state, and most of it is allowed to be a few seconds stale. Deciding precisely what may be stale, and what may not, is the design
Voice is a place, not a call
- Nobody schedules a voice channel or answers it. That moves the hard part from the call to the control plane: placing calls well, moving them silently when a host dies, and not stampeding thousands of SFUs when a million people come back at once
"One server, one ordered process; push the fan-out outwards, and send each person only what they can see."
Self-Assessment Checklist#
After studying this case study, you should be able to:
Fan-out and connections:
- Explain why a community server's fan-out cost grows with the square of its online members
- Design one ordered process per server, with relays and passive sessions for huge servers
- Bound session buffers and make reconnects resume from a sequence number
Presence and storage:
- Combine per-device status into presence, detect offline from session exits, and send member-list changes only to viewers
- Partition messages by channel and time bucket with Snowflake ids, and explain the tombstone trap
- Protect a database from hot partitions with request coalescing and channel-routed data services, and plan a staged migration
Voice and limits:
- Explain how a drop-in voice channel is placed, joined, failed over and end-to-end encrypted
- Design per-route, per-resource rate limits with useful headers, and moderator tools for raids
- Walk through a mass-reconnect incident and fix it by reducing demand before adding supply
Sources#
Discord Engineering (discord.com/blog):
- Blog
Yuliy Pisetsky — Maxjourney: Pushing Discord's Limits with a Million+ Online Users in a Single Server (guild processes; quadratic fan-out; Midjourney 10M+ members, 1M+ online; passive sessions; relays of up to 15,000 sessions; ETS and workers; Manifold Offload), 25 October 2023: https://discord.com/blog/maxjourney-pushing-discords-limits-with-a-million-plus-online-users-in-a-single-server
- Blog
Stanislav Vishnevskiy — How Discord Scaled Elixir to 5,000,000 Concurrent Users (Manifold; consistent hashing and FastGlobal; 500,000 sessions per VM; 5 guilds per user; Semaphore), 6 July 2017: https://discord.com/blog/how-discord-scaled-elixir-to-5-000-000-concurrent-users
- Blog
Matt Nowack — Using Rust to Scale Elixir for 11 Million Concurrent Users (visible-portion member lists; Rust SortedSet), 17 May 2019: https://discord.com/blog/using-rust-to-scale-elixir-for-11-million-concurrent-users
- Book
Austin Whyte — How Discord Reduced Websocket Traffic by 40% (streaming zstandard; READY size; passive session deltas; gateway autoscaling), 20 September 2024: https://discord.com/blog/how-discord-reduced-websocket-traffic-by-40-percent
- Blog
Jesse Howarth — How Discord handles push request bursts of over a million per minute with Elixir's GenStage (demand of 100; buffer and shedding), 12 December 2016: https://discord.com/blog/how-discord-handles-push-request-bursts-of-over-a-million-per-minute-with-elixirs-genstage
- Blog
Stanislav Vishnevskiy — How Discord Stores Billions of Messages (history forever; 120M+ messages a day; 50/50 reads and writes; 10-day buckets; Snowflakes; tombstones), 13 January 2017: https://discord.com/blog/how-discord-stores-billions-of-messages
- Blog
Bo Ingram — How Discord Stores Trillions of Messages (177 Cassandra nodes; data services and coalescing; ScyllaDB migration estimated at 9 days, at up to 3.2M a second; 72 nodes × 9 TB; latencies), 6 March 2023: https://discord.com/blog/how-discord-stores-trillions-of-messages
- Blog
Jesse Howarth — Why Discord is switching from Go to Rust (Read States service), 4 February 2020: https://discord.com/blog/why-discord-is-switching-from-go-to-rust
- Blog
Vicki Niu — How Discord Indexes Trillions of Messages (Lucene's ~2 billion document limit; cells; 40 clusters), 24 April 2025: https://discord.com/blog/how-discord-indexes-trillions-of-messages
- Blog
Jozsef Vass — How Discord Handles Two and Half Million Concurrent Voice Users using WebRTC (gateway, guilds and voice; C++ SFU; placement, failover and DDoS; 850+ servers, 13 regions; 2.6M+ users, 220+ Gbps, 120 Mpps), 10 September 2018: https://discord.com/blog/how-discord-handles-two-and-half-million-concurrent-voice-users-using-webrtc
- Talk
Stephen Birarda — Meet DAVE: Discord's New End-to-End Encryption for Audio & Video (~200M people a month; MLS; frame encryption the SFU can't read), 17 September 2024: https://discord.com/blog/meet-dave-e2ee-for-audio-video
- Talk
Mark Smith — Every Voice and Video Call on Discord Is Now End-to-End Encrypted (complete in early March 2026; stage channels excepted), 18 May 2026: https://discord.com/blog/every-voice-and-video-call-on-discord-is-now-end-to-end-encrypted
- Blog
Josh Stratton — How It All Goes Live: An Overview of Discord's Streaming Technology, 5 March 2024: https://discord.com/blog/how-it-all-goes-live-an-overview-of-discords-streaming-technology
- Blog
David Chen — How We Moved Discord Voice to the Edge (80%+ of traffic on Cloudflare; one SFU host per call; Iceland 2.7× ping; NIC queue packet loss; Rust SFU; expand ratio), 9 June 2026: https://discord.com/blog/how-we-moved-discord-voice-to-the-edge
- Blog
Bo Ingram and Stephen Birarda — You've Got (Too Much) Mail: Behind the Scenes of the 3/25/26 Voice Outage (17% of sessions; gateway and resume; voice syncers and 25,000+ SFUs; timeline and fixes), 29 April 2026: https://discord.com/blog/behind-the-scenes-of-the-3-25-26-voice-outage
- Blog
Nick Krichevsky — Tracing Discord's Elixir Systems (Without Melting Everything) (guild timings; recipient-scaled sampling; presence service), 4 March 2026: https://discord.com/blog/tracing-discords-elixir-systems-without-melting-everything
- Blog
Discord — Blocking Spam Gets Easier Thanks to New AutoMod and Safety Tools (20M+ messages removed; mention spam; paused invites), 21 September 2022: https://discord.com/blog/new-anti-spam-raid-automod-safety-update
Discord company page and developer documentation:
- Read
Discord — Company ("90M+ Daily Active Users", internal data as of Q4 2025): https://discord.com/company
- Docs
Discord Developer Docs — Gateway (heartbeat jitter; resume; IDENTIFY and event limits; sharding; intents; compression): https://github.com/discord/discord-api-docs/blob/main/developers/events/gateway.mdx
- Docs
Discord Developer Docs — Gateway Events (large_threshold; 75,000-member guilds; member chunks): https://github.com/discord/discord-api-docs/blob/main/developers/events/gateway-events.mdx
- Docs
Discord Developer Docs — Rate Limits (per-route, global and invalid-request limits; headers and scopes): https://github.com/discord/discord-api-docs/blob/main/developers/topics/rate-limits.mdx
- Docs
Discord Developer Docs — Voice Connections (voice websocket, UDP, IP discovery, DAVE): https://github.com/discord/discord-api-docs/blob/main/developers/topics/voice-connections.mdx
- Docs
Discord Developer Docs — Channel resource (slowmode 0–21,600 s) and Reference (Snowflakes): https://github.com/discord/discord-api-docs/blob/main/developers/resources/channel.mdx · https://github.com/discord/discord-api-docs/blob/main/developers/reference.mdx
Design note: the architecture beyond what Discord's posts describe, the code, thresholds, dashboards and every number labelled as an assumption or estimate are an illustrative design, not Discord's internals. Peak concurrency, sessions per user, message and event rates, recipients per message, presence change rates, voice users today, bytes per message and all of Lobby 91's numbers are assumptions. Ishaan Kulkarni, Lobby 91 and its tournament are fictional. Discord's published numbers come from posts between 2016 and 2026 and describe the system at the time of each post.
Further Reading#
Related systems to study:
- Read
WhatsApp (Bonus Problem 2 in this series): one-to-one and group messaging on Erlang, with offline delivery and end-to-end encryption of text
- Read
Zoom (Bonus Problem 23): scheduled meetings, SFU forwarding, simulcast and cascading across regions
- Read
Twitter / X (Bonus Problem 10): fan-out on write versus read for a follower graph
- Docs
Figma (Bonus Problem 15): one authoritative process per document, the same idea as one process per guild
From the war stories:
Foundational concepts (this course):
- Read
Week 1, Day 1 — Partitioning Deep Dive
- Read
Week 1, Day 3 — Rate Limiting at Scale
- Read
Week 1, Day 4 — Hot Keys and Skew
- Read
Week 1, Day 5 — Session Store Design
- Read
Week 3, Day 3 — Backpressure and Flow Control
- Read
Week 4, Day 3 — Thundering Herd
- Read
Week 5, Day 1 — Consistency Models
- Read
Week 9, Day 2 — Noisy Neighbour
- Read
Week 9, Day 5 — Security Architecture
- Read
Week 10, Day 4 — Capacity Planning
"One server, one ordered process; push the fan-out outwards, and send each person only what they can see."
this lesson in shorts · 4
play them all →One idea from this lesson in about a minute each, made for your phone.
- 0/7stages cleared
New lessons by email
New lessons, side quests and case files as they ship, at most once a week. No spam, unsubscribe anytime.
Discussion · …
Public: everyone reading this lesson sees comments. Be kind and keep it on the material.



