How Does Discord Work?

TL;DR

  • Discord runs text and voice as separate systems, because messages must never be lost but audio can't wait.
  • Text goes through an HTTPS API and a Gateway WebSocket. Messages are stored in ScyllaDB, which replaced Cassandra in 2022.
  • Voice setup uses a WebSocket, and the audio travels over UDP through a selective forwarding unit (SFU) that copies packets to everyone in the call.
  • Since March 2026, calls are end-to-end encrypted with the DAVE protocol. Text is not.

Before Discord, talking to friends while you played a game took real effort. Like, being in the same room as them! Or you could use voice chat services like TeamSpeak, Ventrilo, or Mumble. But that meant someone in the group had to rent or run a server and hand out an IP address and port. Yes, Skype existed, but it could expose your IP address to the people on the call, and getting knocked offline mid-match by a sore loser was a real thing.

And you needed an entirely different setup for chat, on a forum or in IRC.

Discord blew all that away. One app for all your pwning. You join a server from an invite link, the text channels keep every message anyone has ever sent, and the voice channels are always there. Just join and enjoy.

How? Today Discord stores trillions of messages. Single servers have had more than a million people online at once, and since 2025, voice traffic has moved to an edge network with locations in over 300 cities. Discord's engineers have written about it in detail, but here we're going to go over what is known about how Discord works, how it stores and serves text and voice from millions of users, and what you can learn if you're looking to build your own platform for the world's gamers.

How Is Discord Put Together?

Text and voice are completely different mediums, and need opposite things from a network.

A chat message must never be lost, but nobody minds if it takes half a second to appear. Audio can't wait that 500 milliseconds, but you can drop the packet and move on.

Understandably, then, Discord runs the two as separate systems and the client (web, mobile, or desktop) holds connections to both.

Text and State Voice and Video
Client connections HTTPS API and the Gateway WebSocket A second "voice" WebSocket and a UDP socket
What travels over them Messages, reactions, presence, permissions, who's in which voice channel Opus audio and video frames in RTP packets
Transport TCP UDP for media, TCP for the voice WebSocket
What's on the backend API servers, Elixir guild and session processes, Rust data services, ScyllaDB Voice servers, each running a signaling component and an SFU

What the app calls a server is called a guild inside Discord (engineers keep the word "server" for actual machines). A guild records members, roles, channels, and permissions. One Elixir process per guild routes events to whichever members are connected.

Channels are different, again. A voice channel exists whether or not anyone's in it, the opposite of a Zoom meeting. Nobody schedules anything or sends a link. People join, talk, and leave, and the channel stays put.

The only link between the chat and voice systems is the moment you join a voice channel. The request goes out over the Gateway WebSocket on the chat side, the guild picks a voice server, and your client gets its address back. From then on, the audio never goes near the chat system.

Discord architecture: the client connects to the HTTPS API and Gateway WebSocket for text and state, and to a voice WebSocket and SFU over UDP for voice and video

The two systems also live in different places. The Gateway and the guild processes run in Google Cloud. Voice servers are spread around the world so that one is near you, because distance adds latency to every packet.

How Does Your Client Stay Connected?

A chat client can't keep asking the server whether anything's new. With millions of people online, polling would swamp the API, and messages would still show up late. So the client talks to Discord in two ways:

  • The HTTPS API is for doing things. Sending a message, adding a reaction, changing a setting, and loading older history are all ordinary REST calls.
  • The Gateway is for hearing about things. It's a WebSocket that the client opens at startup and keeps open, and Discord pushes events down it as they happen.

Setting up the Gateway goes like this:

  1. The client connects, and Discord sends a Hello message containing a heartbeat_interval.
  2. The client sends an Identify message with its token. Discord answers with a Ready event, which includes a session_id and the guilds the user belongs to.
  3. Events start arriving. Each one has a type (t), a payload (d), and a sequence number (s).
  4. Every heartbeat_interval milliseconds, the client sends a heartbeat containing the last sequence number it received.

A new message arriving looks like this:

{
  "op": 0,
  "t": "MESSAGE_CREATE",
  "s": 1042,
  "d": {
    "id": "1551636673593475079",
    "channel_id": "613425648685547541",
    "author": { "id": "268473310986240001", "username": "Potatoaim", "global_name": "PotatoAim" },
    "content": "hello",
    "timestamp": "2026-09-21T16:50:11.000000+00:00"
  }
}

The sequence number is what lets a shaky connection recover. If your laptop drops off Wi-Fi for a few seconds, the client reconnects and sends a Resume message with its session_id and the last sequence number it saw, and Discord replays the events it missed. If the gap was too long, Discord rejects the resume. The client then sends an Identify message again and starts a new session. Nothing is replayed that time, so the client loads the current state from scratch.

On Discord's side, the other end of that WebSocket is an Elixir process. Every connected client gets its own session process, and every guild gets a guild process. Elixir runs on the Erlang VM, the same runtime WhatsApp uses, and for the same reason. It was built to run millions of small processes that send messages to each other.

How Does a Message Get to Everyone in the Channel?

Say PotatoAim types "hello" in #general, in a server he shares with Lagw1zard and CtrlAltDefeat.

  1. PotatoAim's client sends a POST to /channels/{channel.id}/messages over HTTPS.
  2. The API checks his token, checks that his roles allow him to post in #general, and checks that he isn't over the rate limit.
  3. The message gets an ID. Every ID on Discord is a Snowflake. It's a 64-bit number, and the top 42 bits are a millisecond timestamp counted from the first second of 2015, so sorting by ID also sorts by time.
  4. The message is written to the messages database.
  5. The guild process for that server picks up the new message and fans it out to the session process of every member who's online and allowed to see #general.
  6. Each session process pushes a MESSAGE_CREATE event down its client's Gateway connection, and hello appears on Lagw1zard's and CtrlAltDefeat's screens.

So when does Lagw1zard see it? If she has Discord open, at step 6. If her connection blipped, the Resume replay delivers it a moment later. If she's been offline for a week, nothing was held for her. When she opens #general, her client asks the API for the channel's recent messages, which is the same call it makes when you scroll back.

WhatsApp handles this the other way round. It queues messages for offline users and deletes them once they're delivered. Discord keeps everything, so the stored history does the queue's job.

How the Same Fanout Works With a Million People Online

Fanout is easy for three friends. For Midjourney's server, which pushed Discord past a million people online at once in a single guild, it's a serious problem. A guild process is a single Elixir process, so it runs on one core, and the fanout work grows with both the number of members and how much they post.

Discord made two changes:

  • Passive sessions. If you're in a server but haven't opened it, Discord marks your session as passive. The guild skips the permission checks for you and sends your client far less until you click in. About 90% of the connections to large servers turned out to be passive, which made the fanout 90% cheaper.
  • Relays. A relay processing layer sits between the guild and the sessions. Each relay handles permission checks and fanout for its own slice of sessions, spreading the work across many cores.

A five-person server works the way it always did, with one guild process sending to five sessions.

How Does Discord Store Trillions of Messages?

Discord decided early on to keep all chat history forever. This means the message table only ever grows. By January 2017, it was taking in more than 120 million messages a day, and Discord found that reads were about as frequent as writes. And because of the way Discord is set up, servers behave differently from each other:

  • A server that's mostly used for voice might send a message or two every few days, fewer than 1,000 a year. Loading its last 50 messages means random disk seeks, and those push other data out of the disk cache.
  • A small private server that's mostly text sends between 100,000 and a million messages a year. Its members only want recent messages, but there are fewer than 100 of them, so they don't ask often enough to keep those messages in cache.
  • A large public server sends millions of messages a year, and thousands of members read the last hour of chat all day long. That data stays in cache.

Discord initially kept messages in one MongoDB collection with a compound index on channel_id and created_at. That lasted until about 100 million messages in late 2015, when the data and the index stopped fitting in RAM and latency became unpredictable. They then moved to Cassandra.

Building your own app? Get access to our Livestream or Video Calling API and launch in days!

How the Messages Table Is Laid Out

Discord describes Cassandra as a KKV store, where the two Ks make up the primary key:

  • The first K is the partition key, which decides which node a row lives on and where it sits on disk.
  • The second K is the clustering key, which identifies a row within its partition and sets the order in which rows are stored.

Every message query is only about one channel, so channel_id became the partition key. created_at couldn't be the clustering key, because two messages can be created at the same time. The message ID could, because it's a Snowflake and already sorts by time. That gave a primary key of (channel_id, message_id).

A channel can run for years, so a partition keyed on the channel alone would keep growing, and Cassandra handles very large partitions badly. Discord added a bucket to the partition key, a number for the ten-day window a message falls in, and the 2023 version of the table still uses the same key:

CREATE TABLE messages (
  channel_id bigint,
  bucket int,
  message_id bigint,
  author_id bigint,
  content text,
  PRIMARY KEY ((channel_id, bucket), message_id)
) WITH CLUSTERING ORDER BY (message_id DESC);

The bucket comes straight out of the message ID. Take the millisecond timestamp in its top 42 bits, divide by the number of milliseconds in ten days, and round down. PotatoAim's "hello" from earlier lands in bucket 428.

To load a channel's recent messages:

  1. The API works out the current bucket and the bucket the channel was created in. A channel ID is a Snowflake too, so the creation time is already in the ID, and no message can be older than the channel.
  2. It reads the newest bucket's partition. Rows are stored newest first, so this is the top of the partition.
  3. If that doesn't return enough messages, it moves to the bucket before and keeps going until it has enough or reaches the channel's first bucket.

A busy channel gets everything it needs from the first partition. A channel that's been silent for three months makes the API walk back through nine empty buckets before it finds anything. Discord accepted that because active servers are the majority, and the query code remembers which buckets came back empty for a channel and skips them the next time.

How Discord Got From Cassandra to ScyllaDB

By early 2022, the cluster had grown from 12 nodes in 2017 to 177, held trillions of messages, and was paging the on-call team regularly. Latency was unpredictable, compactions kept falling behind, and JVM garbage collection pauses sometimes got bad enough that an operator had to reboot a node by hand.

Hot partitions were a big part of it. Take an @everyone announcement in a server with hundreds of thousands of members. They all open the same channel within seconds and ask for the same rows, which all sit in one partition on three nodes. Reads cost more than writes in Cassandra, so those nodes fell behind. Discord reads and writes at quorum, so every other query that touched those nodes slowed down with them.

Discord fixed it in two places:

  • The first fix sits in front of the database. Rust "data services" run between the API and the database. If a thousand requests for the same row arrive at once, the first one starts a query, and the other 999 subscribe to its result, so the database is asked once. Discord calls this request coalescing. Requests are routed by channel ID, so everything for one channel reaches the same instance, and more duplicates get caught.
  • The second was the database itself. In May 2022, Discord moved the messages cluster to ScyllaDB, which had been on its list to explore since 2017. ScyllaDB speaks the same query language as Cassandra but is written in C++, so there's no JVM garbage collector to pause it.
Cassandra ScyllaDB
Nodes 177 72
p99 latency, fetching history 40 to 125 ms 15 ms
p99 latency, inserting a message 5 to 70 ms 5 ms

The off-the-shelf migration tool estimated three months to move the data. The team wrote its own migrator in Rust, which peaked at 3.2 million messages a second and finished in nine days. Seven months later, during the 2022 World Cup final, every goal showed up as a spike in Discord's message graphs. The cluster took the load without trouble.

How Does Joining a Voice Channel Work?

Audio can't go through the Gateway. A WebSocket runs over TCP, and when TCP loses a packet, it holds back everything behind it until the retransmission arrives. Nobody notices that in a text chat. In a conversation, it's a half-second freeze followed by a burst of stale audio. So Discord sends audio over UDP and uses WebSockets only to set up the call.

When PotatoAim clicks a voice channel:

  1. His client sends a Voice State Update over the main Gateway, naming the guild and the channel.
  2. The guild picks a voice server. All the voice channels in a guild share one voice server, so if people are already talking, PotatoAim is sent to theirs. If he's first in, the guild assigns the least loaded voice server in the region. That's the "Awaiting Endpoint" status you sometimes catch in the client.
  3. The Gateway sends back two events. One contains a session_id. The other, Voice Server Update, contains a token and the voice server's address.
  4. The client opens a second WebSocket to that address and identifies itself with the session ID and token. The voice server replies with an IP address, a UDP port, an SSRC that will label PotatoAim's audio stream, and the encryption modes it supports.
  5. The client opens a UDP socket to that IP and port. PotatoAim is probably behind a home router doing NAT, so the client first sends a small IP discovery packet, and the voice server replies with the public IP and port it saw. The client reports those back over the voice WebSocket, along with the encryption mode it picked.
  6. The voice server replies with a 32-byte secret key. The client sends a Speaking message, and audio starts flowing.
Sequence diagram of a Discord client joining a voice channel: Voice State Update, Voice Server Update, Identify, Ready, IP discovery, Select Protocol, Session Description, then encrypted Opus audio over RTP

From there, PotatoAim's microphone audio is encoded with Opus at 48 kHz, wrapped in RTP packets, encrypted with that key, and sent over UDP. The voice WebSocket stays open for the small stuff, such as who's speaking, who joined, and heartbeats.

Discord's desktop and mobile apps cut a lot out of standard WebRTC to make this quick. WebRTC normally describes a call with SDP, which can run to ten kilobytes, and hunts for a network path with ICE. The native apps skip both. Every client connects to a Discord server with a public address, so there's no peer-to-peer path to find, and the whole exchange above comes to about a kilobyte. The browser client has no such freedom, since it has to use the browser's own WebRTC stack.

How Do Audio and Video Get From One Person to Everyone Else?

With six people in a voice channel, PotatoAim's audio has to reach five other people. If his laptop sent five copies, his upload would grow with the channel size, and everyone in it would learn everyone else's IP address.

Discord puts a server in the middle. Each voice server runs a selective forwarding unit, or SFU. PotatoAim uploads one stream, and the SFU copies his packets to the other five by reading each packet's header and forwarding it, without decoding or mixing any audio. Each receiving client holds packets briefly in a jitter buffer, decrypts them, decodes them, and mixes the voices itself.

Camera video and Go Live screen shares take the same route, as RTP packets over UDP to an SFU that forwards them without decoding. A Go Live stream has to be watchable for every viewer, so the streamer won't send more than the slowest viewer's connection can take. Conferencing tools such as Zoom avoid that with simulcast, where the sender uploads several qualities, and the server picks one for each viewer.

Routing every call through a server also means:

  • Nobody on the call sees your IP address. Only Discord does, which ends the DDoS problem that came with peer-to-peer calls.
  • Moderation happens in the SFU. When a moderator server-mutes someone, the SFU drops that person's packets.
  • Nothing is sent while you're quiet. The native apps stop sending packets when you stop talking. In a big channel, only a few people talk at once, so that's a large saving, and sequence numbers are rewritten so the gap doesn't look like packet loss.

Voice servers fail, and since they're the only part of the backend exposed directly to the internet, they get DDoSed as well. Recovery is the same in both cases. The dead server drops out of service discovery, the guild picks a new one and pushes the channel's state to it, and every client receives a new Voice Server Update and reconnects. You hear a short dropout and the call carries on.

Discord picks one SFU to host an entire call, and every participant stays connected to that machine for the duration of the call. Calls aren't split across regions the way Zoom does it with cascaded media servers. So a call's quality depends heavily on where that one host is.

In 2018, the host was one of about 850 voice servers in 13 regions. Since 2025, Discord has been moving voice and video onto Cloudflare's edge network, which has locations in over 300 cities, and by June 2026 more than 80% of that traffic ran there. Discord has also been rolling out a new SFU written in Rust to replace the original C++ one. Each host runs eight worker threads, and each thread has its own UDP socket.

How Are Calls Encrypted End to End?

Every call goes through Discord's servers, so for years the honest answer to "can Discord hear this?" was yes. Audio was encrypted between your client and the voice server, using the secret key from the handshake, and the server had that key too.

That changed with DAVE, Discord's end-to-end encryption protocol for audio and video. The rollout started in September 2024, and since early March 2026 every call is end-to-end encrypted by default, across DMs, group DMs, voice channels, and Go Live streams. Clients that don't support DAVE can no longer join calls.

In a DAVE call:

  1. Everyone in the call joins a Messaging Layer Security (MLS) group. MLS is an IETF standard for agreeing on keys in a group whose membership keeps changing.
  2. Each participant derives their own sender key from the group's secret, and encrypts every audio and video frame with AES-128-GCM before it's split into packets.
  3. When someone joins or leaves, the group moves to a new epoch and everyone's keys change. A newcomer can't decrypt what was said before they arrived, and someone who left can't decrypt what comes after.

The voice server is still involved. It acts as the MLS group's external sender, which lets it propose adding and removing members as people connect and disconnect, and it never holds the group's keys. The transport encryption from the handshake is still there, wrapped around the already encrypted frames. The SFU can read the RTP headers it needs for routing and can't decode the media inside.

The protocol and the implementation are both public, and Trail of Bits audited them.

Stage channels are the exception. They're built for broadcasting to large audiences, and Discord doesn't end-to-end encrypt them. Text isn't end-to-end encrypted either, and Discord says it has no plans to change that because many of its features rely on the server being able to read messages. Search, AutoMod, and every bot that reads message content work that way.

How Did Discord Get Here?

The first version of Discord was built in under two months in early 2015, with everything stored in a single MongoDB replica set. There was no ScyllaDB cluster, no relays, and no edge network. Each piece arrived when the last one stopped coping.

If you're building a Discord-like app yourself, that's the encouraging part. You can start with the tools that exist today for chat, voice, and video, and worry about a million people in one server when you have them.