Can Go Compete with C for In-Memory Databases? Benchmarking Tellstone, Redis, and Valkey

Introduction

There are several threads on reddit challenging the question: Go's performance (with/without GC) good enough to be a competitor to C or Rust. Especially when it comes to maximum performance, predictable latencies, and raw throughput.

As I'm working with Go since 2013 I know all the statements and all the concerns about this. BUT I asked myself if there is a possibility to proof them all.

To test the boundaries of the language, I built Tellstone (TSD): a stripped‑down, RESP‑compatible in‑memory database written completely in Go. I now have a solid foundation, and I wanted to checkout if TSD has a chance to grow in an environment where performance matters. To check that out, I benchmarked it against redis and valkey on my own system.

The results were fascinating, and soon I'll check out if it really can beat them also in the real world!

TL;DR

Table of Contents


The Contenders & Tellstone’s Architecture

TBH redis and valkey are optimized and battle proofed systems that were created over decades! A standard TCP Go implementation won't work out to get the same performance as these projects do.

Go systems usually degrade under extreme workloads due to two main pain points: CPU overhead from system calls handling millions of concurrent network sockets, and latency spikes caused by Stop‑The‑World (STW) garbage collection sweeps.

I tried to design Tellstone with the zero-allocation philosophy which should theoretically reduce these limitations:

Feature Tellstone Implementation
Multi‑Reactor Networking via gnet Event‑driven networking framework gnet binds native epoll instances directly to dedicated CPU cores (instead of a goroutine per connection).
Lock‑Free Hot Path & Sharded Storage 256 independent shards; power‑of‑two masking (hash & (shardCount‑1)) directs reads/writes to separate locks, dropping mutex contention to near‑zero.
Zero‑Allocation RESP Parser RESP parsing operates directly on gnet's receive buffers using slice views, avoiding heap allocations for request parsing. Serialization buffers are recycled via sync.Pool.
Deactivating GC on the Hot Path The request path is designed to avoid heap allocations. During benchmarking, automatic GC is disabled via debug.SetGCPercent(-1), removing GC work from the measured hot path and helping keep latency stable.

The Benchmark: Pushing Sockets to the Limit

I wanted to guarantee reproducibility for the benchmarking. I also isolated the server(s) and the benchmark loader using OS‑level core pinning (taskset on WSL2/Ubuntu):

Hardware Stack:

OS & Kernel Tuning:

Software Stack:

Persistence:

Redis:

Valkey:

Tellstone:

Results: Benchmark Results

These results represent a single workload on a specific hardware and software configuration. They should not be interpreted as universal rankings of database performance. Different datasets, value sizes, workloads, persistence settings, networking stacks, or deployment environments may produce different results.

Metric Tellstone (TSD) Redis (v7.x) Valkey (v8.x)
Throughput (Ops/sec) 2,404,495 1,996,341 2,018,534
Relative Throughput 120.4 % 100 % (baseline) 101.1 %
p50 Latency 3.103 ms 3.167 ms 3.135 ms
p99 Latency 5.183 ms 5.279 ms 5.151 ms
p99.9 Latency 7.359 ms 7.231 ms 7.007 ms
Throughput (RPS) — Higher is Better
====================================
Tellstone: ██████████████████████████████ (2.40M RPS)
Valkey:    █████████████████████████       (2.01M RPS)
Redis:     ████████████████████████        (1.99M RPS)

All measurements were performed inside WSL2 rather than on bare-metal Linux. All systems were evaluated under the same environment.

Latency Analysis: The Back‑pressure Effect

Percentiles show Tellstone remains stable under pressure. At p50 it clocks 3.103 ms, beating both Redis and Valkey. Even at p99 and p99.9, Tellstone stays neck‑and‑neck with the hand‑optimized C engines.

“Average Latency” Phenomenon

Memtier reports a high average latency ≈ 163 ms for Tellstone, versus ~3.2 ms for Redis/Valkey. This is network saturation back‑pressure: Tellstone pushes 2.4 M RPS, 20 % more load than Redis, saturating the kernel’s TCP stack. A small fraction of connections incur queuing delay inside the OS socket buffers before the reactor can process them. The engine‑level percentiles stay low, proving the processing itself is in the microsecond range; the bulk of the “average” latency is likely caused by OS queue time.

Why Tellstone performs well

Reason Explanation
Multi-reactor networking gnet distributes sockets across multiple epoll loops, spreading load across all pinned cores.
Sharded storage Independent shards reduce lock contention and improve parallelism.
Cache locality Zero-allocation request processing keeps hot data structures compact and cache-friendly.
Minimal GC work The hot path avoids heap allocations, and automatic GC is disabled during benchmarking.

Next Milestones: Pushing Go to Its Absolute Limit

Achieving > 2.4 M RPS is a fantastic milestone, but the journey continues. I’m exploring:

  1. Transition to io_uring – Evaluate experimental Go networking libraries that leverage kernel‑bypass socket batching.
  2. Strict Shared‑Nothing Model – Each reactor exclusively owns a set of shards, eliminating the final lock barrier.
  3. Asynchronous Non‑Blocking Snapshots – Lock‑free snapshot engine using copy‑on‑write to stream state to disk without pausing the event loops for even a single microsecond.

These results suggest that, with careful engineering, Go can achieve throughput comparable to—and in this benchmark exceeding—well-established C implementations for this class of workload.

Repository: https://github.com/Saxy/Tellstone (run the benchmark suite, inspect the code).

Every benchmark shown in this article can be reproduced via the included Taskfile.

task bench:compare \
    TARGET=tellstone \
    THREADS=8 \
    CONNS=100 \
    PIPELINE=8
task bench:compare \
    TARGET=redis \
    THREADS=8 \
    CONNS=100 \
    PIPELINE=8
task bench:compare \
    TARGET=valkey \
    THREADS=8 \
    CONNS=100 \
    PIPELINE=8