HomeResourcesCase study
Case study

Why Discord's Latency Had a Heartbeat

By The SDL team·4 min read·Updated Sep 16, 2026

A two-minute latency spike with no traffic cause isn't a scaling problem. It's your runtime confessing.

Discord rewrote a hot service from Go to Rust. The internet decided it was a language war. It wasn't. It was a story about who controls memory — your runtime, or you.

The service was "Read States" — the thing that tracks which messages you've read, hit on nearly every action in the app. It worked. It was fast. And every two minutes, like clockwork, its latency spiked.

Plain English

Some programming languages clean up unused memory for you. A background process — the garbage collector — periodically walks through everything you've allocated, figures out what's no longer needed, and frees it. Convenient. You never have to think about memory.

The catch: while it's walking, your program can pause. Usually that's invisible. But if you're holding a huge pile of objects in memory, the collector has a lot to walk through — and the pause gets long enough to notice. Now imagine that pause lands on a service every user touches, every couple of minutes.

The two-minute heartbeat of pain

Discord's Read States service held a large in-memory LRU cache of user state. In Go, the garbage collector ran on a schedule and re-scanned that cache even when there was no memory pressure — there was simply nothing Discord could do to tell it "don't bother, we're fine." Every couple of minutes, the collector did its rounds, and latency jumped. Not because of traffic. Because of the runtime.

Latency over time: the Go sawtooth vs. the Rust flatline slow fast time → GC pause Go every ~2 min Rust microseconds, flat The spikes weren't load. They were the runtime stopping the world to collect garbage — and Go re-scanned the entire LRU cache on a schedule, whether memory was tight or not.
The sawtooth that isn't load. Periodic latency spikes with no traffic cause are the signature of a garbage collector, not a hot path.
Now the engineering

Rust doesn't have a garbage collector. Instead it uses an ownership model: the compiler tracks, at build time, exactly when each piece of memory is no longer reachable, and frees it deterministically right then. There's no background process and no stop-the-world pause — because freeing memory isn't a separate event, it's woven into the code's normal flow.

The result wasn't subtle. The periodic spikes vanished entirely. Tail latency dropped to the microsecond range with a millisecond-scale ceiling, and — freed from worrying about GC scan cost — they could grow the cache to 8 million entries without the pauses getting worse.

~2 min
old spike interval
8M
cache entries, no penalty
µs
new tail latency

Worth knowing

This is not "Rust beats Go." Go's GC is excellent for the vast majority of services. The point is narrower and more useful: when a workload holds a large, long-lived heap on a latency-critical path, the cost of automatic memory management can dominate — and that's the specific case where manual control pays for itself. Reach for the rewrite for that reason, not for fashion.

How to recognise this in the wild

The diagnostic signature is the giveaway: latency spikes that are periodic and uncorrelated with traffic. If your p99 has a heartbeat, you're not looking at a load problem — you're looking at your runtime. Profilers will confirm it: time spent in GC, mark-and-sweep phases, allocation rate against heap size.

The gap it reveals

"Rust has no garbage collector" is a fact a junior can recite. Predicting that a large in-memory cache on a hot path will make GC the bottleneck — and reading a periodic latency spike as a memory-management signature rather than a scaling problem — is the judgment that separates someone who has heard of GC from someone who has been burned by it.

In the interview room

If your design parks a big cache in front of a database, expect: "what language, and why?" A strong answer connects the runtime to the workload — "GC'd language is fine here because the heap is small and churny; if this cache grew to millions of long-lived entries I'd worry about pause times and consider a non-GC runtime." That sentence alone signals you've operated systems, not just drawn them.

The reframe

Every convenience in engineering is a trade you haven't been billed for yet. Automatic memory management is a wonderful default — until your workload sends the bill. Senior engineers aren't the ones who avoid conveniences; they're the ones who know which convenience their specific workload will turn into a liability, and exactly when.

The language didn't win. The engineer who understood the workload did.

Primary source →
discord.com — Why Discord is Switching from Go to Rust

Want feedback on your design?

The weekly teardown

One real-world architecture, every week.

How real companies actually built it: the design, the trade-offs, and what to say about it in an interview. Free, and one click to unsubscribe.

Related articles