At a glance#
| Number | Value | What it drives |
|---|---|---|
| Daily active users | 90M+ (Discord, Q4 2025) | ~31M sessions at peak (assumed) |
| Largest server | 10M+ members · 1M+ online (2023) | relays, passive sessions, workers |
| Sessions per Erlang VM | up to 500,000 (2017) | ~125 session nodes at 50% (estimated) |
| Sessions per relay | up to 15,000 (2023) | ~67 relays for a million online |
| Dispatches (assumed) | ~6M / s at peak | fan-out first, storage second |
| Messages | trillions · 72 ScyllaDB nodes (2023) | (channel, 10-day bucket) partitions |
| Voice | 2.6M+ users · 220+ Gbps (2018) · 25,000+ SFU instances (2026) | one host per call; ~85 kbit/s each (derived: 220 Gbps ÷ 2.6M) |
| API limits | 50 req/s per bot · 10,000 invalid per 10 min | per-route, per-resource buckets |
Architecture#
DiagramFour planes: connections, guild state, storage, media
%% title: Four planes: connections, guild state, storage, media
flowchart TD
APP@{ icon: "sd:desktop", label: "Clients<br/>desktop · phone · web · console" }
GW@{ icon: "tech:websocket", label: "Gateway · Elixir<br/>websockets · zstd-stream" }
API@{ icon: "sd:api", label: "API · Python<br/>REST · rate limits" }
SESS["Sessions<br/>one process per device"]:::service
GUILD["Guild process<br/>one per server · relays"]:::hot
PRES["Presence<br/>status · activities"]:::core
DS["Data services · Rust<br/>coalesce by channel"]:::service
DB@{ icon: "sd:database", label: "ScyllaDB<br/>messages · 72 nodes" }
SYNC["Voice syncers<br/>place calls on SFUs"]:::core
SFU@{ icon: "tech:webrtc", label: "SFU fleet<br/>25,000+ · UDP" }
ETCD@{ icon: "tech:etcd", label: "etcd<br/>hash ring · discovery" }
PUSH@{ icon: "tech:firebase", label: "Push · FCM<br/>buffer · shed bursts" }
APP ==> GW
APP ==> API
APP -.->|"UDP media"| SFU
GW <==> SESS
API ==> DS ==> DB
API ==>|"dispatch · gRPC"| GUILD
GUILD ==>|"fan-out"| SESS
GUILD --> SYNC --> SFU
GUILD -.-> PUSH
PRES --> SESS
ETCD -.-> GUILD
class APP client
class GW,API edge
class SFU external
class DB,ETCD store
class PUSH queue- In over HTTP, out over the websocket: a message is stored before anyone is told, so a dropped socket loses only the live copy.
- The guild is the unit: one process orders a server's events and holds its members, roles, channels, voice states and sessions.
- Media is separate: the guild passes voice states to the voice syncers, which pick a call's SFU host; audio and video go client to SFU over UDP.
DiagramZoom-in · a huge guild
%% title: Zoom-in · a huge guild
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 with workers" }
W["Worker<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 clientKey flows#
DiagramAn @everyone from send to 71,204 screens
%% title: An @everyone from send to 71,204 screens sequenceDiagram autonumber participant I as Sender 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 · nonce · @everyone A->>A: rate limit · MENTION_EVERYONE A->>D: insert · routed by channel id D->>S: write (channel, bucket) · Snowflake id S-->>A: stored A-->>I: 200 with message id A->>G: dispatch MESSAGE_CREATE G->>R: one send per relay R->>M: permission-checked · next sequence number Note over G,M: passive sessions catch up in the next passive update
DiagramA dropped socket resumes, a lost session starts over
%% title: A dropped socket resumes, a lost session starts over
sequenceDiagram
autonumber
participant C as Client
participant GW as Gateway
participant S as Session process
C->>GW: last sequence 9,312 · session id
GW->>S: is the session alive
alt session alive · events still in replay buffer
S-->>GW: events 9,313 to 9,340
GW-->>C: replay in order · RESUMED
else session gone or too far behind
S-->>GW: invalid session
GW-->>C: identify again
C->>GW: IDENTIFY · admitted by start-session limit
GW-->>C: READY · lazy guild stubs
end
Note over C,GW: jittered reconnects spread the herdDiagramDrop into a voice channel, then survive a dead host
%% title: Drop into a voice channel, then survive a dead host
sequenceDiagram
autonumber
participant C as Client
participant G as Guild process
participant VS as Voice syncer
participant V as Voice server
C->>G: voice state · join channel
G->>VS: channel voice states
VS-->>C: endpoint and token · call already placed
C->>V: voice websocket · UDP discovery · MLS join
V-->>C: Voice Connected · encrypted Opus
alt host dies or is DDoS'd
V--xC: voice websocket drops
C->>G: request a new endpoint
G->>VS: re-place the call · push voice states
VS-->>C: new endpoint
C->>V: reconnect to the new host
endData model#
DiagramCore entities
%% title: Core entities
classDiagram
class Guild {
+guildId : snowflake · hash ring key
+roles : list
+channels : list
+relays : list · above a size threshold
}
class Member {
+userId : snowflake
+roles : list
+nick : string
}
class Channel {
+channelId : snowflake
+type : text or voice
+overrides : per role and member
+slowmodeSeconds : int · 0 to 21,600
}
class Message {
+channelId : snowflake · partition key
+bucket : int · about 10 days
+messageId : snowflake · clustering key
+authorId : snowflake
+content : string · only non-null columns written
}
class Session {
+sessionId : string
+userId : snowflake
+status : online, idle, dnd, invisible
+seq : int · replay buffer
+passive : bool · per guild
}
class VoiceState {
+userId : snowflake
+channelId : snowflake
+sfuHost : string · one host per call
+mlsEpoch : int · changes on join or leave
}
Guild "1" --> "many" Member : has
Guild "1" --> "many" Channel : has
Channel "1" --> "many" Message : stores
Member "1" --> "many" Session : connects as
Member "1" --> "0..1" VoiceState : in voice- Messages partition by (channel id, ~10-day bucket) to stay under 100 MB; Snowflake ids sort by time and give the bucket.
- Read states (unread counts, mentions) are one record per user per channel, kept in a separate service.
Lifecycle#
DiagramA voice call, from first join to empty
%% title: A voice call, from first join to empty stateDiagram-v2 [*] --> AWAITING_ENDPOINT: first member joins AWAITING_ENDPOINT --> CONNECTED: host chosen, UDP both ways CONNECTED --> CONNECTED: members drop in or out, new MLS epoch CONNECTED --> MOVING: host dies or is DDoS'd MOVING --> CONNECTED: re-placed, clients reconnect MOVING --> FAILED: no capacity in region CONNECTED --> ENDED: last member leaves ENDED --> [*] FAILED --> [*] class CONNECTED ok class FAILED bad class MOVING queue
Decisions#
| Decision | Chose | Over | Because |
|---|---|---|---|
| Unit of state | One ordered process per guild | Broker topic per channel | permissions and order in one place |
| Huge guilds | Relays · passive sessions | Splitting the guild | some views need the whole server |
| Member list | Visible ranges only | Full list to every client | presence would be quadratic |
| History reads | Coalesce, route by channel | Message cache | no invalidation on edits |
| Partitions | (channel, 10-day bucket) | Channel only | bounded size, spreads over cluster |
| Voice transport | SFU, one host per call | Peer-to-peer | IP privacy, big rooms, moderation |
| Overload | Bounded buffers, fail fast | Unbounded queues | protect the node, then resume |
When it breaks#
| Failure | What users see | Contain it with |
|---|---|---|
| Guild mailbox grows | a laggy server | relays · shed typing and presence · workers |
| Slow client socket | a reconnect | buffer cap · resume by sequence |
| Hot channel read | slow history everywhere | coalescing · channel-routed data services |
| Voice host dies | seconds of silence | re-place call · push voice states |
| Mass session loss | Awaiting Endpoint | drain before scale-down · spawn limits · more syncers |
| Spam raid | flooded chat | slowmode · AutoMod · pause invites |
Say it in the interview#
- Fan-out is quadratic: in a community everyone talks and everyone listens, so ten times the people is a hundred times the work.
- One ordered process per server: it holds roles and sessions and orders events; relays do the sends, 15,000 sessions each.
- Send only what's seen: passive sessions get deltas, member lists update by visible range, counts lag by seconds.
- The session isn't the socket: sequence numbers and a replay buffer turn a drop into a resume, not a full READY.
- Store, then dispatch: messages live in (channel, bucket) partitions, and coalescing turns 20,000 identical reads into one.
- A voice channel is a place: placed once on one SFU host by where its members are, moved silently when the host dies.
- Reduce demand before adding supply: a mass reconnect needs spawn limits first, then capacity, as Discord's March 2026 outage showed.