BetterSWE DigestWhat is BetterSWE?

System Design · Issue 1

How Discord Stores Trillions of Messages

By 2022, Discord's message store ran on 177 Cassandra nodes and paged its on-call team most nights. Here is how a new C++ engine plus a Rust tier in front of it got them to 72 calm nodes.

Pixel-art illustration of sharded database drums holding message cards with a request gateway in front.

The constraint

A database that worked, and still hurt

A large cluster of database nodes with an on-call engineer being paged

By early 2022 Discord's message store held trillions of messages across 177 Cassandra nodes. Capacity was fine and the data was correct at every scale. What had degraded was predictability: the on-call team got paged constantly, tail latency wandered, and routine maintenance had become too expensive to run.

Discord fixed the pain on two fronts at once. They swapped the storage engine for ScyllaDB, and they built a Rust service tier in front of it to shield the database from its own worst access pattern. Most retellings focus on the engine swap, but the service tier they put in front of it is the part worth copying.

A database can be correct at trillions of messages and still be too costly to keep running.

Origins

From one MongoDB box to Cassandra

A single MongoDB database outgrown and migrated to a larger Cassandra cluster

The first Discord was built in under two months in early 2015, and everything lived in a single MongoDB replica set, deliberately, with a planned exit. Around November 2015 they hit 100 million stored messages, and the data plus its index no longer fit in RAM. Latencies turned unpredictable, and it was time to move.

In 2017 they migrated to Cassandra, chosen for good reasons: linear scaling with no manual re-sharding, automatic failover, proven at scale at Netflix and Apple, and open source. The promise that quietly mattered most was low maintenance, and it was the first one to break at 177 nodes.

  1. 2015 (Single MongoDB replica set)
  2. Nov 2015 (100M messages, no longer fits RAM)
  3. 2017 (Cassandra, 12 nodes, billions of messages)
  4. 2022 (177 nodes, trillions of messages)

Cassandra was the right call for years, meeting a genuine and well-reasoned list of requirements. The 2023 story is what happens when a system grows an order of magnitude past the point where those assumptions held.

Data model

Partitioned by channel, bucketed by time

A channel's messages grouped into time buckets and replicated across three database nodes

Every ID in Discord is a Snowflake, chronologically sortable. Messages are partitioned by the channel they were sent in, along with a bucket, a static ~10-day time window. All messages for a given channel and bucket sit together, replicated across three nodes at replication factor 3.

primary key: ((channel_id, bucket), message_id)

  channel 42 ─┬─ bucket 0  → [msg, msg, msg ...]
              ├─ bucket 1  → [msg, msg ...]
              └─ bucket 2  → [msg, msg, msg ...]

  read recent → walk buckets newest-first
                until enough messages collected
The partition scheme carried over unchanged from Cassandra to ScyllaDB.

The bucket exists for a reason. Cassandra warns about partitions over 100MB, since large partitions cause pressure during compaction and can't be distributed. Time-bucketing keeps each partition small. To read recent messages, the service generates a bucket range from now backward and queries partitions in sequence until it has enough.

The pain

Hot partitions, compaction, and the GC

One database partition overheating as many reads converge on it

In Cassandra, reads cost more than writes. A write appends to a commit log and an in-memory memtable; a read must consult the memtable and potentially many on-disk SSTables. Pile enough concurrent reads on one partition and you get a hot partition, and because queries use quorum consistency, every node serving that partition slows down. One busy channel could raise latency across the whole cluster.

The toil underneath

Two more taxes stacked on top. Nodes fell behind on compaction, forcing a manual "gossip dance": pull a node from rotation, let it compact, bring it back to catch up via hinted handoff, repeat. And the JVM garbage collector produced pauses ranging from latency spikes to stop-the-world stalls long enough that an operator had to reboot and babysit the node.

The tombstone war story from 2017

A public Puzzles & Dragons server deleted millions of messages, leaving one. Loading that channel forced Cassandra to scan millions of tombstones, generating garbage faster than the JVM could collect it, causing constant 10-second GC pauses. Fix: drop tombstone lifespan from 10 days to 2, and track and skip empty buckets. This tombstone theme comes back to bite during the migration.

The shield

Fixing a database problem outside the database

A Rust service tier in front of the database coalescing many identical requests into a single read

Discord was suspicious that a shiny new database alone would fix everything, because hot partitions are an access-pattern problem, not an engine bug. So before touching the engine, they built data services in Rust: thin intermediary services between the API monolith and the database, roughly one gRPC endpoint per query, with no business logic.

  API monolith
       │  route by channel_id (consistent hash)
       ▼
  ┌─────────────┐   all requests for one channel
  │ data service│   land on the same instance
  └─────────────┘
       │  coalesce: N identical requests → 1 query
       ▼
   ScyllaDB (one read serves everyone)
Consistent-hash routing plus request coalescing collapse a traffic spike into a single query.

The core trick is request coalescing. If many users request the same row at once, the first spins up a worker task and the rest subscribe to it; the worker queries the DB once and hands the result to everyone. Paired with consistent-hash routing on channel ID, every request for a channel lands on the same instance, so coalescing actually catches the duplicates.

An @everyone announcement on a huge server, the classic hot-partition maker, gets absorbed into one database read. Discord is honest that this did not cure hot partitions on Cassandra; it made them rarer and bought time to prepare ScyllaDB. Rust fit because its fearless concurrency and the Tokio ecosystem made safe concurrent code easy to write.

The engine

Same interface, no garbage collector

A JVM engine stalling on garbage collection beside a smooth shard-per-core C++ engine

ScyllaDB is Cassandra-compatible but written in C++. The appeal was concrete: a shard-per-core architecture for stronger workload isolation, faster repairs, and, above all, no JVM to tune. As Discord puts it plainly, ScyllaDB is not void of issues, it is void of a garbage collector.

One blocker remained. In testing, reverse queries (scanning against a table's sort order) were too slow for Discord's read path. The ScyllaDB team prioritized and shipped performant reverse queries, an upstream feature driven directly by Discord's use case, which cleared the last obstacle.

The super-disk storage topology

Each ScyllaDB node runs on a super-disk: local SSDs for speed mirrored via RAID to a persistent network disk for durability. On GCP this is a hybrid RAID1, where writes hit both local NVMe and the persistent disk, reads prefer NVMe, and a restarted node rebuilds from the persistent copy. It gets attached-disk speed with network-disk durability.

The migration

Nine days, 3.2 million messages a second

A firehose of message rows moving from a Cassandra cluster into a ScyllaDB cluster at high speed

The requirement: move trillions of messages with no downtime, and do it fast, because Cassandra was still catching fire. The first plan dual-wrote new data to both databases and backfilled history with ScyllaDB's Spark migrator. After tuning, the estimate came back at three months, plus a pile of dual-world complexity. Rejected.

Instead they extended their existing Rust data-service library into a migrator, in an afternoon. It reads token ranges from Cassandra, checkpoints progress locally in SQLite, and firehoses rows into ScyllaDB. The new estimate: nine days, fast enough to abandon the time-based cutover and migrate everything at once.

Spark migrator

~3 months, dual-world complexity

Rust migrator

9 days, up to 3.2M msgs/sec

The migrator hit speeds up to 3.2 million messages per second, then jammed at 99.9999%. The final token ranges kept timing out because they held gigantic runs of tombstones Cassandra had never compacted away. Compacting that range finished the migration seconds later. The tombstones from 2017, one last time.

The aftermath

72 nodes and a flat World Cup graph

A smaller 72-node cluster replacing a 177-node cluster with a flat latency graph

They cut over in May 2022, validating by mirroring a small fraction of reads to both databases and comparing results. The node count dropped from 177 Cassandra nodes to 72 ScyllaDB nodes, each holding 9 TB versus Cassandra's ~4 TB average, roughly half the machines and half the disk.

read p99:40–125 ms→15 mswrite p99:5–70 ms→5 ms\begin{aligned} \text{read p99} &: 40\text{--}125\,\text{ms} \rightarrow 15\,\text{ms} \\ \text{write p99} &: 5\text{--}70\,\text{ms} \rightarrow 5\,\text{ms} \end{aligned}
Historical-fetch and insert tail latency, Cassandra to ScyllaDB.

The tail latencies collapsed and, more importantly, stopped wandering. During the 2022 World Cup Final, one of the busiest traffic moments Discord had seen, the latency graph stayed flat. The system finally had headroom to spare.

The takeaway

The two-front fix

Why it matters

What makes this worth copying is that Discord treated one database problem as two. The engine swap killed the GC pauses and compaction toil, but the Rust data-services tier is what actually tamed hot partitions, and that lives outside the database entirely. Reaching for a new datastore without fixing the access pattern in front of it would have carried the same hot-partition failure into ScyllaDB.

The short version

  • Cassandra was correct at trillions of messages; its **operational cost** at 177 nodes is what broke, driven by GC pauses, compaction toil, and hot partitions.
  • A **Rust data-services tier** shields the DB via request coalescing and consistent-hash routing, collapsing an `@everyone` spike into a single query.
  • **ScyllaDB** (C++, shard-per-core, no GC) cut the cluster to **72 nodes** and drove read p99 to **15ms**, write p99 to **5ms**.
  • A Rust migrator moved everything in **9 days** at up to **3.2M msgs/sec**, versus a 3-month Spark estimate, snagging at 99.9999% on old tombstones.
  • Fixing the access pattern in front of the database mattered as much as swapping the engine underneath it.

Sources

  1. 1.How Discord Stores Trillions of Messages (Discord Engineering, 2023)
  2. 2.ByteByteGo | How Discord Stores Trillions of Messages
  3. 3.How Discord Stores Billions of Messages (And Why They Had to Rebuild It Three Times) | by Himani Prasad | Medium
  4. 4.How Discord Stores Trillions of Messages with High Performance
  5. 5.How Discord stores trillions of messages (2023) | Hacker News
  6. 6.How Discord Stores TRILLIONS of Messages Without Breaking — The Secret | by Atul Programmer | Medium
  7. 7.How Discord Stores TRILLIONS of Messages | by Mohammed Vaghjipurwala | Medium
  8. 8.How Discord Stores Trillions Of Messages . | by Mehtab | Medium
  9. 9.How Discord Stores Billions of Messages? | by Saharkhiz Company | Medium
  10. 10.How Discord Stores Trillions of Messages - Better Engineers
  11. 11.How Discord Stores Trillions of Messages | by Dr Milan Milanović | Medium
  12. 12.How Discord Stores Trillions of Messages
  13. 13.Lessons from Discord's tryst with Mongo, Cassandra and Scylla
  14. 14.How Discord Migrated Trillions of Messages from Cassandra to ScyllaDB - ScyllaDB
  15. 15.How Discord Migrated Trillions of Messages from Cassandra to ScyllaDB
  16. 16.How Discord Migrated Trillions of Messages to ScyllaDB - The New Stack
  17. 17.Discord Migrates Trillions of Messages from Cassandra to ScyllaDB - InfoQ
  18. 18.ScyllaDB and Discord. A Migration Success Story with Trillions of Messages. | by Minhaaj Rehman | Medium
  19. 19.How Discord Migrated Trillions of Messages from Cassandra to ScyllaDB - YouTube
  20. 20.Discord Migrated Trillions Of Messages To ScyllaDB - Undercode Testing
  21. 21.Scaling Trillions of Messages: Discord's Journey from Cassandra to ScyllaDB with Rust-Powered Solutions 🚀 - Blog by Saifeddine Rajhi
  22. 22.How Discord Uses Rust in 2026: The Read Service Rewrite and Beyond - Rustify
  23. 23.GitHub - serenity-rs/serenity: A Rust library for the Discord API. · GitHub
  24. 24.Why Discord is Moving an Many Services from Go to Rust
  25. 25.serenity - Rust
  26. 26.Discord - Rust Forge
  27. 27.How Discord Stores Billions of Messages (Discord Engineering, 2017)
  28. 28.How Discord Supercharges Network Disks for Extreme Low Latency
  29. 29.Why Discord is switching from Go to Rust