Patterns

Partition messages by channel: Discord

Discord stored trillions of messages in a wide-column store, partitioned by channel and time, and read with a quorum.

Discord moved its messages from MongoDB to Cassandra in 2017, when it had 12 nodes and billions of messages. By early 2022 the cluster had 177 nodes and trillions of messages, and the team migrated it again, to ScyllaDB. The schema partitions messages by channel and a time bucket, and replicates each partition three times.

The system#

Every message row is keyed by channel and bucket, so a channel's recent messages sit together and a read is a range scan. Reads and writes use quorum consistency. The weak spot is the hot partition: a server with hundreds of thousands of members sends far more traffic to one channel than a small server does. Discord added data services in front of the store, with consistent-hash routing by channel, so requests for one channel coalesce on one service instance.

The board#

Drag to pan. Hold Ctrl or Cmd and scroll to zoom.

At 4.00k Users the Simulation solves this Design to Apdex 0.98, p99 87 ms, 2.0% errors and $76/s Burn. The board is the message store’s shape at game scale. The data services that coalesce hot channels are not on it. The caption is Quorum's result, not the company's numbers.
WhereSettingValueWhy
Load BalancerPolicyleast-connA gateway connection is long-lived, so the load balancer sends it to a hub with room.
App ServerInstance sizelargeLarge, because the API tier does the routing and the reads.
App ServerCopies3Three copies stand for the API tier in front of the message store.
Wide-column StoreReplicas per key (N)3Three copies of each key, as the store keeps.
Wide-column StoreReplicas a read asks (R)2Two of three replies, the quorum Discord reads with.
Wide-column StoreReplicas a write waits for (W)2Two of three, so a read that asks two overlaps the latest write.
Wide-column StoreInstance sizemediumMedium for each node, so a hot partition shows.
CacheEngineredisRedis is the store Discord uses for presence and hot state beside the message store.
CacheCache strategyasideThe API fills the cache on a miss, so a popular channel is read from memory.
CacheTTL (seconds)60A short TTL keeps hot channel reads current.
CacheInstance sizesmallSmall, because it holds hot state and not the message log.
Realtime HubStackbeamBEAM holds a lightweight process per connection, which is why Discord’s gateway fits so many on a box.
Realtime HubClusteringOnSeveral hubs sit behind one pub/sub backbone, so one box does not take every connection.
Realtime HubReconnect backoffOnClients reconnect with jitter after a restart, so the gateway is not hit by every client at once.
Realtime HubPresenceOnThe hub tracks who is online, which costs CPU on each join.
Link Load Balancer → App ServerKinds carriedpage,read,write,loginMessages and reads go to the API tier.
Link Load Balancer → Realtime HubKinds carriedrtRealtime events go to a hub, not to an app server.

Research this Design needs: Load balancing, Leaderless replication, Caching, WebSockets, Leaderless replication, WebSockets, Caching, Elixir.

The Wide-column Store is the message cluster, with three replicas per key and a read and write quorum of two. The Realtime Hub is the gateway tier, on BEAM because a lightweight process per connection is what lets one box hold many. The Cache holds hot channel state.

Why it works#

A quorum read of two replicas overlaps the latest write of two, so a read sees the last message even after a node fails. Partitioning by channel keeps a conversation's history contiguous. Quorum reads and writes and hot keys and hot shards are the two ideas the schema leans on.

Where the game stops#

The board has one Wide-column Store, not 72 ScyllaDB nodes, and it does not show the data services or the per-channel partitions. The caption is Quorum's result, not Discord's p99. The game's Hot key incident is the same failure the data services were built to absorb.

Sources#