~learn.

Discord, the whole design.

A real-time community platform for 90 million+ daily users, with servers of up to a million online, that keeps one ordered process per server and pushes the fan-out outwards (relays, passive sessions, lazy member lists), stores messages by channel and time bucket, and places each drop-in voice call on one SFU host.

7 diagrams · 8 sections · revise in ~10 min · Download PDF prints it in the light theme, diagrams included

SQ-31 · Discord · design sheethimanshukukreja.in/learn/sheets/bonus-31-discord-voice-and-servers

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
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 client

Key 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 herd
DiagramDrop 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
  end

Data 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

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#