agni
rust · go · kotlinan in-memory cache server implemented three times from the same design, to measure what the language actually changes.
the problem
"which language is faster?" is a bad question because it is almost never asked with the same design on both sides. the comparisons that circulate usually measure different programs, written by different people, solving similar problems.
agni tries to answer the honest version of it: holding the wire protocol, the workload shape and the benchmark harness constant, how much difference is left when only the language changes?
architecture
a cargo workspace with four crates: agni (store, protocol and command logic), agni-server (tcp binary), agni-client (cli) and agni-bench (benchmark). the client and the benchmark speak exactly the same protocol as the server, so measurement exercises the real path.
all three implementations live in one clone, preserved in history: main carries the rust line, and the go and kotlin versions sit at tags snapshot/go and snapshot/kotlin. no parallel branches left to rot: one git checkout gets you each language's full tree.
the scope is deliberately small: no replication, no clustering, no persistence, no multi-node coordination. the goal is not to compete with redis, it is to isolate the variables that matter for the comparison.
decisions and trade-offs
`DashMap` over `Arc<RwLock<HashMap>>`. with 50 concurrent connections contending on 1,000 keys, the RwLock serialises every write through a single lock. dashmap shards across 64 buckets and gains 28.5% throughput on SET, with p99 dropping from 1.52ms to 1.07ms. the cost: pure cache misses got ~9% slower, because a miss under a read lock is extremely cheap and shard selection adds overhead. trade-off accepted, since in a warm cache the miss is the rare path.
a byte-based parser instead of `String::from_utf8_lossy`. Command::from_bytes now splits the byte slice directly, which lets binary values survive a round trip. it sits in the per-command hot path, so it was measured before and after.
interleaved A/B/A/B benchmarking. the first attempt ran four "after" runs and then four "before" runs, and showed an apparent 3 to 9% improvement. it was entirely machine drift across the batch. interleaving the runs put the parser's real gain inside the noise. the change was kept for correctness, because binary survives the round trip, not for performance.
benchmarks
go vs rust, throughput in ops/sec, 50 connections, 10,000 operations per scenario, local loopback, release build.
| scenario | go | rust | delta |
|---|---|---|---|
| PING | 262,976 | 478,315 | 1.8x |
| SET | 193,132 | 466,359 | 2.4x |
| GET (hit) | 260,325 | 470,338 | 1.8x |
| GET (miss) | 270,682 | 466,458 | 1.7x |
| Mixed SET+GET | 201,805 | 451,237 | 2.2x |
measured 2026-08-25. the full methodology and history live in BENCHMARK.md in the repository. results are appended, never replaced, so the series stays auditable.