Muon

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.

The workload

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.

Internal benchmark

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.

6.4×
Higher peak throughput
Lower p99 latency
20×
Lower memory footprint
70×
Faster warmup
MetricMuon (Rust)Umami (Node/Next.js)
Peak ingest throughput~5,800 req/s~900 req/s
p99 latency @ 50 concurrent15 ms111 ms
Idle app memory~12 MB~275 MB
Peak app memory under load29 MB605 MB
Docker image160 MB1.28 GB
Warm start → serving0.16 s11.2 s
App CPU per event0.36 ms1.34 ms
  reproduce it
# 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 real story

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.

What Rust buys

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.
What Rust costs

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.

Why it matters

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.

FAQ

Questions, answered directly.

Is Rust overkill for an analytics backend?
For the query and UI layer, probably. For the ingest path — millions of tiny, latency-sensitive writes running around the clock — per-event runtime cost is the dominant expense, and a compiled, GC-free language reduces it by roughly 4× in our internal benchmark (0.36 ms vs 1.34 ms app CPU per event).
Are these benchmark numbers independent?
No — this is an internal benchmark run by the Muon team on a 10-core / 8 GB Docker host. The full harness ships in the repository (./bench/run.sh) so you can reproduce the run on your own hardware; treat the ratios as directional.
Is all of Muon written in Rust?
The backend — ingest, sessionization, rollups, the statistical detection jobs — is Rust. The web UI is React served as static files, because that layer changes frequently and benefits from the TypeScript ecosystem's iteration speed.
Should my team rewrite its backend in Rust?
Only if your workload looks like ours: a stable, hot path where per-request cost multiplies by enormous volume. If your bottleneck is developer iteration rather than runtime overhead, Rust's compile times and smaller hiring pool will cost you more than the CPU it saves.
Open source · self-hosted

See the numbers on your own hardware.

Deploy Muon with Docker in minutes, or read the full internal benchmark with methodology and caveats.