Why we built an analytics backend in Rust
Because an ingest path that costs almost nothing per event changes what the product can afford to do: in our internal benchmark, Muon's Rust core sustains ~5,800 requests per second at 15 ms p99 on hardware where a Node equivalent peaks near 900.
Is Rust worth it for an analytics backend?
For an always-on event ingest workload, yes. In our internal benchmark on a 10-core / 8 GB host, Muon's Rust backend reached ~6.4× the peak throughput of Umami's Node backend (~5,800 vs ~900 req/s), with 15 ms vs 111 ms p99 latency, ~12 MB vs ~275 MB idle memory, a 160 MB vs 1.28 GB image and 0.16 s vs 11.2 s warmup. The honest price is slower iteration and a smaller hiring pool — worth paying for infrastructure with a stable core, rarely for fast-changing product code.
Ingest never sleeps.
An event pipeline is the worst possible fit for a heavyweight runtime: millions of tiny writes, latency-sensitive, always on.
Most web backends spend their lives waiting — for a user, for a query, for a cache. An event ingest endpoint is different. Every page a visitor opens becomes a small HTTP request that must be parsed, validated, sessionized and written, thousands of times per second, around the clock. There is no idle period to hide in and no request is individually interesting enough to justify overhead.
That shape dictates the runtime choice. Per-event cost is multiplied by every event you will ever receive, so a runtime that burns 1 ms of CPU before your code even runs is not a rounding error — it is most of your infrastructure bill. We wanted the per-event cost of the application layer to be as close to zero as engineering allows, and that pointed at a compiled language with no garbage collector: Rust.
Same events. Same Postgres. Different ceilings.
We drove identical traffic — 111,000 events from 5,000 distinct visitors through the same Umami-compatible /api/send endpoint — into Muon and into Umami v3, each with its own PostgreSQL 16 on the same 10-core / 8 GB host.
| Metric | Muon (Rust) | Umami (Node/Next.js) |
|---|---|---|
| Peak ingest throughput | ~5,800 req/s | ~900 req/s |
| p99 latency @ 50 concurrent | 15 ms | 111 ms |
| Idle app memory | ~12 MB | ~275 MB |
| Peak app memory under load | 29 MB | 605 MB |
| Docker image | 160 MB | 1.28 GB |
| Warm start → serving | 0.16 s | 11.2 s |
| App CPU per event | 0.36 ms | 1.34 ms |
# the harness ships in the repository — one command, throwaway containers
git clone https://github.com/runmuon/muon && cd muon && ./bench/run.sh
This is an internal benchmark run by the Muon team, not an independent evaluation. Full methodology, caveats and the raw concurrency ramp are on the benchmark page; absolute numbers vary by hardware — the ratios are the point.
The bottleneck moves. That's the point.
The most useful result isn't any single ratio — it's where each system hits its ceiling.
Under load, Muon becomes PostgreSQL-bound while the Node system stays runtime-bound. Muon's application layer spends 0.36 ms of CPU per event; the database spends 0.59 ms. Umami's application layer alone spends 1.34 ms per event — more than Muon's app and database combined — before a single query is issued. Push it harder and the ramp shows it: Muon holds ~5,300–5,800 req/s from 50 to 400 concurrent connections with p99 degrading gracefully from 15 to 95 ms, while the Node backend caps near 900 req/s and its p99 climbs from 122 ms to 903 ms.
Why this matters for scaling: a database bottleneck has a rich, well-understood toolbox — batching, indexing, partitioning, read replicas, and eventually analytical storage. A runtime bottleneck has essentially one tool: more replicas of an expensive process, paying the same per-event tax on each. Choosing Rust didn't remove the ceiling; it moved the ceiling to the layer where decades of engineering can raise it.
Predictable. Small. Nearly free.
The wins are not abstract language advocacy — each one shows up as a number in the table above.
- Predictable latency. No garbage collector means no collection pauses hiding in the tail. The p99 gap (15 ms vs 111 ms) is mostly the absence of a runtime that periodically stops the world.
- Tiny images, instant replicas. A 160 MB image that serves its first request 0.16 s after boot makes autoscaling honest: a new ingest replica is useful in milliseconds, not eleven seconds into a traffic spike.
- A footprint that rounds to zero. 12 MB idle and 29 MB under load means Muon runs comfortably next to your app on the smallest VPS you own — self-hosting stops being a capacity-planning exercise.
- Headroom for understanding. When ingest costs 0.36 ms of CPU per event, the compute budget left over can be spent watching every signal — which is the actual product.
Honest costs. We pay them daily.
Rust is not a free upgrade, and pretending otherwise would undermine the numbers above.
- Iteration speed. Compile times are real, and the borrow checker taxes exploratory code. A quick endpoint experiment that takes minutes in Node takes noticeably longer in Rust.
- Hiring pool. Far fewer engineers write production Rust than production TypeScript. For a team that expects to grow fast around this codebase, that is a genuine constraint.
- Ecosystem depth. The web ecosystem is solid but thinner than Node's — some integrations you would install in npm get written by hand here.
The calculus works because an event ingest core is infrastructure with a stable contract: the endpoint shape is fixed (Umami-compatible, deliberately), the write path changes rarely, and correctness plus per-event cost dominate everything else. Where iteration speed matters — the UI, the site, the exploratory edges — Muon uses TypeScript and React like everyone else. Rust is the right tool for the layer that runs a billion times, not for the layer that changes every week.
Cheap compute funds understanding.
The performance isn't the product. It's what makes the product affordable.
Muon's goal is not to show you charts faster — it is to notice what changed before you look. That means continuously running statistical detection (robust z-scores, two-proportion tests, false-discovery-rate control) across every metric × segment cell of your product, on hourly rollups, every day. This findings engine is taking shape in the repository now, and its economics only work because the layer underneath it is nearly free: the same 10-core host that a heavyweight runtime saturates just receiving events has, with a Rust core, most of its capacity left over for investigation.
That is the real answer to "why Rust". Not benchmark bragging rights — a cost structure where watching every signal, in every segment, all the time, is cheap enough to be the default. If you are weighing Muon against the system it is wire-compatible with, the feature-by-feature comparison with Umami covers the rest.
Questions, answered directly.
Is Rust overkill for an analytics backend?
Are these benchmark numbers independent?
Is all of Muon written in Rust?
Should my team rewrite its backend in Rust?
See the numbers on your own hardware.
Deploy Muon with Docker in minutes, or read the full internal benchmark with methodology and caveats.