DEV Community

Mubashir Taj
Mubashir Taj

Posted on

How Discord Handles Millions of Concurrent Voice Users

Voice chat is unforgiving. A web page can load half a second late and nobody notices. A voice call that stalls for half a second is immediately obvious, and a call that drops packets for several seconds is unusable. Discord has to deliver that experience to millions of people at the same time, across thousands of servers, in dozens of regions, on connections ranging from fiber to congested mobile networks. The architecture that makes it work is not one clever trick. It is a set of deliberate tradeoffs: use UDP instead of TCP, forward audio instead of mixing it centrally wherever possible, keep voice state separate from chat state, and push failure handling to the edge where it can be recovered quickly.

To understand the design, it helps to separate three problems that are often confused. First, how does a client find and join a voice channel? Second, how does audio move between participants with low latency? Third, how does the system stay stable when a server fails, a region degrades, or a single channel suddenly grows from ten people to ten thousand listeners? Each problem has its own component, and each component can scale and fail independently.

Discord splits its real time work across two kinds of gateway. The main gateway handles presence, messages, typing indicators, and the general state of a guild. The voice gateway handles one thing only: voice sessions. When you join a voice channel, the client receives a voice server endpoint, a session identifier, and a token. It then opens a separate WebSocket connection to that voice gateway and negotiates a UDP connection for the actual media. This separation matters more than it first appears. Chat traffic is bursty and can tolerate short delays. Voice traffic is continuous and cannot. If both shared the same connection and the same backpressure logic, a spike in message traffic could stall voice signaling. By isolating voice, Discord protects the latency sensitive path from everything else happening in the app.

Once the session is established, audio flows over UDP. TCP guarantees delivery and ordering, which sounds desirable until you think about what it costs in a live call. If a TCP packet is lost, the protocol pauses delivery of everything after it until the missing packet is retransmitted. In a file download that is correct behavior. In a voice call it produces exactly the wrong result: old audio arrives late, new audio is held back, and the conversation stutters. UDP makes no such promise. Packets can arrive out of order or not at all, and the application decides what to do about it. For voice, the right decision is usually to move on. A twenty millisecond slice of audio that arrives late is worse than useless, because the conversation has already moved past it. Dropping it and playing the next slice keeps the call feeling live.

The audio itself is encoded with Opus, a codec designed for interactive speech. Opus adapts its bitrate to the available bandwidth, handles both speech and music reasonably well, and includes packet loss concealment, which means the decoder can synthesize a plausible continuation when a packet goes missing. That last property is easy to underestimate. At low loss rates, good concealment makes a lossy network sound almost clean. Discord also relies on jitter buffers on the receiving side. Packets on the internet do not arrive at perfectly even intervals, so the client holds a small buffer, typically tens of milliseconds, and plays audio out of it at a steady rate. A larger buffer absorbs more jitter but adds latency. A smaller buffer keeps latency low but risks gaps. Tuning that tradeoff, and adapting it as network conditions change, is a large part of what makes a call feel stable.

The forwarding model is the next key decision. In a small call, one naive approach is a full mesh: every participant sends audio directly to every other participant. That works for three or four people and collapses quickly after that, because each client must upload one stream per peer and manage one connection per peer. Discord instead uses voice servers as selective forwarding units. Each participant sends a single upstream stream to the voice server, and the server forwards the relevant streams to each listener. It does not decode and mix all audio into one combined stream, which would cost far more CPU and would remove the client's ability to control individual volumes and mute states. Forwarding keeps the server cheap relative to mixing, and it keeps per user controls intact, since each incoming stream remains separate on the receiving client.

That model also explains why voice servers are regional. Latency is dominated by physical distance and routing, so placing the voice server close to the participants matters directly. Discord assigns a voice region to each guild, and the client connects to a server in that region. When participants are spread across continents, no single region is ideal for everyone, and the practical answer is to pick the region that minimizes the worst case for the active speakers, not to chase a theoretical optimum. Region selection is therefore a configuration choice with real user impact, and large communities change it deliberately when their audience shifts.

Scale brings a different set of problems. A voice server can handle many concurrent sessions, but it cannot handle an unbounded number. Discord shards voice load across a fleet of servers and treats each voice session as movable state with a clear owner. If a voice server process crashes, the sessions on it must be reestablished, and the client must be able to reconnect without the user pressing anything. This is where the separation between signaling and media pays off again. The voice gateway session can be reestablished, a new UDP endpoint can be issued, and the client can resume sending audio, often quickly enough that users perceive a brief glitch rather than a dropped call. Designing for fast reconnection is more valuable than pretending crashes will not happen, because at Discord's scale they happen constantly.

Encryption and authentication are part of the voice path, not an afterthought. Voice packets are encrypted, and the session token issued during join prevents arbitrary clients from injecting audio into a channel. The encryption scheme is designed for real time use: it adds minimal overhead per packet and does not require a handshake per packet. That matters because voice sends roughly fifty packets per second per speaker. Any per packet cost, whether CPU, bytes, or round trips, multiplies immediately. System design for real time media is often a story about per packet costs, and the winning designs are the ones that keep that cost close to zero.

There are lessons here that transfer to much smaller systems. The first is to isolate latency sensitive traffic from everything else. If your application mixes chat, notifications, and voice on one connection, one slow consumer can stall the rest. The second is to choose the transport that matches the semantics you actually need. TCP is the right default for data that must arrive exactly once in order. UDP is the right default for data where timeliness beats completeness. The third is to prefer forwarding over mixing when clients need individual control, and to design reconnection as a normal path rather than an error path. Users forgive a brief glitch. They do not forgive a call that dies and forces them to rejoin manually.

Discord's voice system works at scale because it respects the physics of real time communication. It keeps the media path short, simple, and close to the user. It accepts packet loss instead of fighting it with retransmission. It forwards rather than mixes, isolates voice from chat, and treats reconnection and measurement as core features. Any team building voice, whether for a game, a classroom, or a workplace tool, will face the same constraints at a smaller scale. The answers will differ in detail, but the questions will be the same: how fast must this arrive, what happens when it does not, and how quickly can a broken session come back on its own.

// Voice forwarding is a fanout problem, not a mixing problem.
// Each speaker sends one stream. The server copies it to listeners
// without decoding every stream into a single mixed output.
type VoiceSession struct {
    SpeakerID string
    Listeners map[string]bool
}

func (s *VoiceSession) Forward(packet []byte) {
    for listener := range s.Listeners {
        // In a real server this write is non blocking and
        // drops the packet for a slow listener instead of
        // stalling every other listener.
        sendTo(listener, packet)
    }
}

func sendTo(listener string, packet []byte) {
    // UDP send to the listener endpoint.
    _ = listener
    _ = packet
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)