esmetrics
VictoriaMetrics is one of the fastest open-source time-series databases in existence. Its Go implementation is not naive code that a rewrite trivially beats — it is a decade of careful engineering around the garbage collector: object pools everywhere, unsafe byte↔string casts, buffer recycling as a way of life.
That made it the perfect subject for a question we wanted a rigorous answer to: how much performance is left on the table when the same algorithms run without a GC?
Today we're releasing the answer as EsMetrics, an Apache-2.0 licensed, from-scratch Rust implementation of VictoriaMetrics single-node (reference v1.146.0). On the TSBS benchmark suite it outperforms the original Go server on every metric — ingestion throughput and query latency across all ten devops query types — on both Linux and Windows, while answering queries byte-for-byte identically.
TSBS cpu-only, scale 100 (8.64 million metrics), four workers, medians of three paired back-to-back rounds against the official v1.146.0 release binaries on identical hardware:
Ingestion
| platform | Go v1.146.0 | EsMetrics | delta |
|---|---|---|---|
| Linux (4 cores) | 3.59M metrics/s | 5.14M metrics/s | +43% |
| Windows 11 (8 cores) | 2.99M metrics/s | 4.95M metrics/s | +66% |
Queries — all ten TSBS query types are faster. Mean latency improvements range from −11% to −59% on Linux and −52% to −82% on Windows. The heaviest query in the suite (double-groupby-all, aggregating a thousand series) went from 102ms to 41ms on Windows.
Correctness — 750 replayed TSBS queries produce byte-identical JSON to the Go server, down to Go's shortest-round-trip float formatting. Roughly 600 tests translated from the upstream Go suites run in CI on Linux and native Windows.
All raw per-round data, the harnesses, and the honest caveats live in the repo. The short version of the caveats: two machines, one workload family, and a handful of sub-millisecond stats that flip within the TSBS client clock's ~0.5ms Windows quantization.
The single most valuable decision in the project was a correctness rule: every response must match the Go server byte-for-byte.
That sounds obsessive. It is. It's also the only reason we can publish benchmark numbers with a straight face — a "faster" database that returns subtly different aggregates is just a broken database with good marketing.
The rule had teeth. It forced us to port Go's exact float formatting (Go's strconv produces shortest-round-trip decimal in fixed notation — 1e21 prints as 1000000000000000000000), the exact staleness-interval semantics of the rollup engine, the exact tie-breaking in the deduplication algorithm, and the exact JSON escaping of the response templates. A replay harness diffed our responses against the Go binary on identical data after every optimization. It caught more real bugs than the ported unit tests did.
The straight algorithmic port — faithful data structures, idiomatic Rust — won the ingestion benchmark immediately and lost most of the query benchmarks. Everything after that was profile-driven. A few of the changes that mattered:
1. The parallel unpacking pipeline. Go's netstorage collects per-series block references without decoding, then unpacks blocks across per-CPU workers. Our first port did all of that on the HTTP connection thread; the profile showed 26% of query time in series materialization and 30% in zstd/varint decode, all serialized. Porting the two-pass design — reference collection, then decode+merge across a persistent work-stealing pool with per-worker scratch buffers — cut the heaviest query's latency by 60%.
2. Caching what the upstream re-decodes. VictoriaMetrics decompresses data blocks on every query. We added a size-bounded, sharded decoded-block cache keyed by (part, offset), invalidated on part drop. This is a deliberate deviation from the faithful port — and it's worth 25-60% on read-heavy queries. Deterministic destruction made the invalidation story clean: when the last reference to a merged-away part drops, its cache entries go with it.
3. Allocation discipline where it counts. The ingest path is allocation-free at steady state: a zero-copy Influx parser that borrows from the request buffer (lifetimes prove what Go's unsafe casts merely hope), thread-local conversion arenas, and an ingestion API that never clones metric names. Sharded caches keyed by xxh64 with precomputed hashes — one hash per lookup — replaced the standard library's SipHash maps on the hot path.
4. Small mechanical wins that compound. SWAR varint decoding (eight bytes at a time instead of restart-per-byte). K-way merging of overlapping blocks instead of concatenate-and-stable-sort. A fair round-robin evaluation pool so a heavy query can't starve the sub-millisecond ones behind it.
The most instructive chapter was Windows. Under Wine, everything looked great. On real Windows hardware, multi-series queries were suddenly seven times slower than Go — 748ms for a query Go served in 102ms — while ingestion still won comfortably.
The profile pattern was strange: the degradation scaled linearly with series count, roughly half a millisecond per series. That's not an algorithm problem; that's a per-allocation problem. The Windows default process heap serializes concurrent allocations under a global lock. Go never notices — it ships its own allocator. Rust, by default, uses the system allocator.
One line — #[global_allocator] static GLOBAL: MiMalloc — took that query from 748ms to 81ms.
Windows had two more lessons that required structural fixes rather than tweaks. WinSock's shutdown() doesn't interrupt a thread blocked in recv the way POSIX does, so graceful shutdown needed ticked reads on idle connections. And deleting a merged-away data part's directory costs tens of milliseconds on Windows (hello, Defender) — which occasionally landed on whichever query thread happened to drop the last reference. The fix mirrors what the upstream does: a dedicated background remover thread, with drains at close boundaries so tests and restarts stay correct.
Porting is a brutal code review. Two findings stand out.
The concurrency one: under concurrent ingestion, series registration had a window where two workers could assign different TSIDs to the same new series — in one benchmark load we counted 2,214 "shadow" series, which silently inflated every query that touched the affected hour. Striped single-flight locks closed it. The race pattern exists upstream in a milder form; the Rust port made it visible because our profiling kept asking why Windows queries unpacked 3.2× more series than expected.
The subtle one: our port of Go's regexp/syntax used char::to_lowercase/to_uppercase to approximate Go's unicode.SimpleFold. But simple-fold orbits must be closed cycles, and the std mappings aren't — U+212A (KELVIN SIGN) folds to k, but nothing folds back to it. Any while f != c orbit walk over a negated character class became an infinite loop. If you're implementing case-insensitive matching by hand: bound your orbit walks.
EsMetrics implements what the TSBS benchmark and standard Prometheus-style usage exercises: Influx line-protocol ingestion, the Prometheus query API, the full storage engine, retention, deduplication. It does not (yet) do clustering, the agent/alerting toolchain, other ingestion protocols, or the web UI. Versioning mirrors the upstream: EsMetrics 1.146.x tracks VictoriaMetrics v1.146.0, and a scripted sync process (baselines pinned, upstream diffs auto-mapped to the Rust modules that port them) is how it stays current.
EsMetrics is developed by Softalink LLC, with Claude (Anthropic) as an engineering contributor — the porting blueprints, the profile-driven optimization iterations, and the benchmark harness work were done in close human-AI collaboration. We think the result — a benchmark-complete, byte-identical port of a seriously optimized production database — is an interesting data point on what that collaboration can produce, and all of the evidence is public for scrutiny.
EsMetrics is Apache-2.0, a derivative work of VictoriaMetrics (Copyright VictoriaMetrics, Inc.). If your organization wants commercial support, sponsored features, or benchmark validation on your hardware — we'd love to hear from you.