Why Discord moved Read States from Go to Rust

A short explainer of Discord's 2020 rewrite, plus a small demo that reproduces the workload in four languages.

What is the Read States service?

Every time you open a channel, Discord needs to know which messages you have already read. The Read States service tracks that: one small record per user, per channel, holding things like the last message read and the mention count.

It sits on the hot path. It is hit on every connection, every message sent, and every message read, so it has to be fast all the time, not just on average.

To keep it fast, each instance keeps recently used read states in memory in an LRU cache holding millions of entries, and writes changes back to the database.

The problem: garbage collection spikes

The Go version was mostly fast, but every couple of minutes it showed large latency and CPU spikes.

The cause was Go's garbage collector. Go forces a collection at least every two minutes, and each collection has to scan the whole heap to find what is still alive. With an LRU cache holding millions of long-lived entries, that scan was large even though very little garbage was being created.

Tuning helped only so much. Shrinking the cache made each scan cheaper, but more requests missed the cache and went to the database, so tail latency got worse.

The fix: rewrite it in Rust

Rust has no garbage collector. Memory is freed as soon as its owner goes out of scope, so an evicted cache entry is freed the moment it is evicted. There is no periodic pass over the whole heap.

Go version

  • Garbage collected
  • Spikes every ~2 minutes as the GC scans the cache
  • Bigger cache meant bigger GC pauses

Rust version

  • No GC; ownership frees memory on eviction
  • No periodic spikes
  • Cache could grow without a GC cost

Discord reported that even a fairly basic Rust port matched the tuned Go service, and after some optimization it beat Go on latency, CPU, and memory. Without a GC cost, they could also raise the cache capacity, which cut database reads further.

The demo in this repo

This repository reproduces the shape of that workload so you can watch the difference yourself. Each program runs a single-threaded LRU cache with a capacity of 20,000,000 entries, writes 20,000 random key/value pairs, sleeps 100ms, and repeats forever.

LanguageFileCache
Gomain.gomap + container/list
Rustsrc/main.rsHashMap + index-based linked list
Odinmain.odinmap + index-based linked list
Pythonmain.pyOrderedDict

Run one and watch its CPU and memory in a tool like htop:

go run main.go
cargo run --release
odin run main.odin -file -o:speed
python3 main.py