Discord engineers found a way to add distributed tracing to Elixir’s actor model without the usual slowdown. That matters because tracing often adds...
Discord Engineers Added Distributed Tracing to Elixir’s Actor Model Without Paying the Usual Performance Tax
Discord engineers found a way to add distributed tracing to Elixir’s actor model without the usual slowdown. That matters because tracing often adds overhead, and in systems built on lightweight processes and message passing, even small waste shows up fast. I’ve seen this kind of work before at Google, Microsoft, Amazon, and Zulily, and the winners are always the teams that respect both speed and human effort—good stewardship, plain and simple. — Ernie Castro
Key Takeaways- Discord’s work shows that observability and speed do not have to fight each other.
- The actor model in Elixir, built on the BEAM VM, makes tracing hard because calls hop between processes.
- Careful design can keep tracing useful while avoiding heavy runtime cost.
- This matters for debugging, incident response, and keeping large systems honest.
- The best systems respect both machine time and human time.
What Is Distributed Tracing in Elixir’s Actor Model?
Distributed tracing tracks a request as it moves through many parts of a system. In Elixir, that system often uses the actor model, where tiny processes send messages to each other instead of sharing memory. That is clean. It is also tricky.
Here’s the rub. A single user request may cross many processes, services, and queues before it finishes. If you do tracing badly, you add lag, memory churn, and noise. If you do it well, you get a clean map of where time goes, where messages stall, and where failure hides.
I’ve worked on systems where logs were everywhere but truth was nowhere. Most teams think more logs will save them. Often, that just means more junk. Tracing is better when it follows the path of work, not just random printouts.
The Elixir angle matters because the BEAM VM is built for concurrency and fault tolerance. That is a gift. It also means tracing must fit the grain of the system, not fight it. Discord’s result is notable because it shows tracing can be woven into actor-style message flow without turning every call into a tax collector.
For background on the actor model, the Wikipedia overview of the actor model is a decent starting point, though the real value comes from reading how BEAM systems behave under load. For tracing standards, the OpenTelemetry documentation is the place most teams should start. If you want to understand the runtime side, the Erlang/OTP documentation and the Elixir getting started guide are useful references.

Core Benefits and Why This Matters
The point is not fancy charts. It is control, and control saves time, money, and a lot of panic during incidents.
- Faster debugging: Tracing shows the path of a request, so you can spot the slow hop instead of guessing.
- Better incident response: When a service breaks, traces show where the chain snapped.
- Less guesswork: Logs tell fragments. Traces tell the story.
- Cleaner performance work: You can see whether latency comes from code, queues, network calls, or retries.
- Respect for work: Good tracing respects engineers’ time, which is part of treating labor with dignity instead of wasting it.
I remember a rough week at Amazon when a tiny latency spike turned into a long night. Everyone had theories. Nobody had proof. That is how bad systems waste good people. Tracing is not magic, but it cuts through the fog. Frankly, that alone is worth a lot.
Here is the kicker. In actor systems, work is scattered across many processes. One request can fan out like a dropped bag of rice. Without trace context, you lose the thread. With trace context, you can follow the chain from entry point to final reply.
Discord’s achievement matters because the usual fear is real: tracing adds overhead. Extra metadata, extra propagation, extra storage, extra CPU. The trick is to make context flow cheaply, often by using metadata already carried in messages, minimal copying, and runtime hooks that avoid heavy per-message work.
If you want a deeper read on tracing design, see OpenTelemetry traces and the W3C Trace Context spec. Those standards explain why trace IDs and span context matter in large systems.

How to Add Distributed Tracing Without a Performance Hit
This is where teams usually make messes. They bolt on tracing after the fact, then wonder why the app slows down and the graphs look like a drunk seismograph.
- Define the tracing goal first
Decide what you need to see: request latency, actor handoffs, slow database calls, or cross-service delays. If you trace everything, you usually learn nothing. I’ve watched teams drown in noise because they wanted “full visibility.” That phrase causes trouble. - Propagate context through messages
In actor systems, the trace context must move with the message. That means trace IDs, span IDs, and parent links need a simple path from sender to receiver. Do not stash them in random globals. That road leads to pain. - Keep metadata small
Every extra byte matters when messages fly fast. Use compact structures, avoid unnecessary copying, and do not serialize fat payloads just to pass observability data. Small is good. Waste is not. - Use runtime hooks carefully
On the BEAM, hooks and instrumentation points can help attach tracing with low overhead. The goal is to observe without disturbing the scheduler. That is the whole game. - Sample wisely
Tracing every request sounds noble. It is also expensive. Sample common paths lightly and error paths more aggressively. That gives you useful data without turning the system into molasses. - Measure before and after
Don’t trust your gut. I’ve seen smart people swear a feature was “cheap,” only to find it cost real CPU and memory. Measure throughput, latency, allocations, and GC pressure. - Tie traces to real incidents
The best tracing setup helps with actual failures, not imaginary dashboards. Use traces during outages, retries, slow queues, and downstream failures. That is where the truth lives. - Keep engineers in the loop
Tracing that nobody uses is clutter. If the on-call team cannot read it fast, it failed. Simple rules beat clever junk.
For a practical background on metrics and trace signals, the OpenTelemetry specification is worth a look. For BEAM concurrency details, Erlang processes and message passing explains why this runtime can handle large numbers of lightweight tasks so well.

Comparison Table: Distributed Tracing in Elixir Actor Systems vs. Classic Thread-Based Services
| Aspect | Elixir Actor Model with Low-Overhead Tracing | Classic Thread-Based Service |
|---|
| Concurrency style | Many lightweight processes and message passing | Fewer heavier threads and shared state |
| Trace propagation | Context moves with messages | Context often follows call stacks |
| Failure isolation | Stronger isolation between actors | Shared state can spread bugs faster |
| Performance risk from tracing | Lower if metadata is compact and propagation is careful | Often moderate, but thread-local context can still cost time |
| Debugging clarity | Great for request paths across actors and services | Good for call chains, weaker for async hops |
| Scaling pattern | Suits high-concurrency systems well | Can get messy under very large concurrency |
| Operational fit | Strong for fault-tolerant backends | Strong for simple synchronous apps |
Common Mistakes to Avoid
Most advice here is sloppy. People say, “Just add tracing,” as if performance issues are a minor stain you can wipe off with a napkin.
- Tracing too much: If every internal message becomes a span, you will create noise and overhead.
- Copying large context blobs: Big payloads waste memory and CPU. Keep trace context lean.
- Ignoring async boundaries: Actor systems move work across boundaries. If you lose context there, your trace is broken.
- Using traces as a log dump: Traces are not a trash bin for every debug string.
- Not measuring overhead: If you do not benchmark, you are guessing. Guessing is how teams burn weeks.
- Making the tool too hard to read: If the on-call engineer needs a manual to follow one request, the design is bad.
I’ve seen teams confuse volume with insight. That habit is costly. It is also unfair to the people who must wake up at 2 a.m. and fix the mess. Good systems should not punish the humans who keep them alive. That is just basic justice.
The cleaner pattern is to trace at useful seams: entry points, remote calls, queue hops, and slow external work. Use spans where decisions happen. Skip the dead weight.
If you want a solid primer on tracing strategy, read OpenTelemetry trace concepts and the W3C Trace Context recommendation. Those documents are dry, yes, but they are also the bones under the skin.
Frequently Asked Questions
What is the main benefit of distributed tracing in Elixir?
It shows how a request moves through many actors and services, so teams can find delays and failures faster.
Why is tracing hard in the actor model?
Because work does not stay in one call stack. It hops between processes, so context must move with each message.
Does tracing always slow a system down?
No. With compact context, careful propagation, and sampling, the overhead can stay very small.
What tools are usually used for this?
Most modern teams use OpenTelemetry-style instrumentation, plus backend storage and visualization tools that can read trace spans.
Final Thought
This kind of engineering matters because it respects both the machine and the people behind it. Discord’s work shows that careful design can keep systems observable without turning them sluggish, and that is the kind of craft I admire. I have spent enough years in big shops to know that good software is not just fast; it is honest, readable, and fair to the humans who must carry it. Ermenildo V Castro would call that stewardship, and he would be right.