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.

The constraint
A database that worked, and still hurt

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

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.
- 2015 (Single MongoDB replica set)
- Nov 2015 (100M messages, no longer fits RAM)
- 2017 (Cassandra, 12 nodes, billions of messages)
- 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

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

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

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

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

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

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