12.7 million ops/s In-Memory Database in Go — Here's How We Hit Zero Allocations

TL;DR

Table of Contents


The Benchmark That Started Everything {#the-benchmark-that-started-everything}

We ran memtier_benchmark against four engines on the same 56-core Xeon Platinum 8580 with 118 GB RAM. 256-byte values, 1:10 write:read ratio, pipeline depth 10. Server pinned to specific cores via taskset. Here are the results at 32 CPUs:

Engine         Total ops/s     vs Redis     p50 latency   
Tellstone      12,738,014      11.53x       0.08ms  
Dragonfly      7,286,281       6.59x        0.17ms 
Redis          1,104,924       1.0x         1.15ms        
Valkey         996,006         0.90x        1.27ms        
Throughput (ops/s) — Higher is Better
======================================
Tellstone: ██████████████████████████████ (12.7M)
Dragonfly: █████████████████              (7.3M)
Redis:     ███                            (1.1M)
Valkey:    ██                             (1.0M)

Dragonfly is excellent — it scales from 1.2x Redis at 4 CPUs to 6.6x at 32. But Tellstone maintains a consistent 1.75x lead over Dragonfly at every core count. At 4 CPUs, we're at 2.4M ops/s (2.06x Redis). At 16, we hit 6.8M (5.98x Redis). At 32, 12.7M.

Why Zero Allocations Matter {#why-zero-allocations-matter}

In a high-throughput in-memory store, the CPU isn't the bottleneck. The heap is. Every allocation triggers:

  1. Heap growth → more memory pages to manage
  2. GC pressure → the garbage collector wakes up, stops the world (or does concurrent marking), scans pointers
  3. Cache pollution → allocated objects land in new memory, cache lines miss
  4. Lock contention → the Go allocator uses per-P caches, but at high core counts even that becomes a bottleneck

At 12M ops/s, even one allocation per op means 12 million allocations per second. That's a GC party. At zero allocations, the GC literally never runs. We've verified this: over 200 million combined operations, the Go heap never grows. GC remains completely dormant.

Here's what the micro-benchmarks show:

BenchmarkEngineGetNoAlloc-32          76,606,690    15.95 ns/op    0 B/op    0 allocs/op
BenchmarkEngineSetNoTTL-32            52,341,876    22.14 ns/op    0 B/op    0 allocs/op
BenchmarkEngineGetWithEncryption-32    7,008,223   175.30 ns/op    0 B/op    0 allocs/op
BenchmarkWriteSequential-32            1,884,082   676.70 ns/op    0 B/op    0 allocs/op
BenchmarkReadMessageZeroAlloc-32     856,039,898     1.46 ns/op    0 B/op    0 allocs/op
BenchmarkParseSQL_Select-32           38,451,227    26.00 ns/op    0 B/op    0 allocs/op

GET: 16 nanoseconds, zero bytes allocated.

SET: 22 nanoseconds, zero bytes. Even with ChaCha20-Poly1305 encryption enabled: 175 nanoseconds, zero bytes.

The RESP parser, the SQL parser, the WAL writer — everything reports 0 B/op, 0 allocs/op.

How We Achieved It: The Stack {#how-we-achieved-it}

Layer 1: Storage Engine — Pre-Hashed Sharded Map

Tellstone uses a shared-nothing sharded map. Each shard owns a disjoint key range. No locks, no CAS, no mutex. Keys are pre-hashed (xxhash) during parsing, so the storage engine receives a uint64 hash — no re-hashing, no string comparison.

// Keys arrive as uint64 hashes, not strings
func (s *Shard) Get(keyHash uint64) ([]byte, bool) {
    entry, ok := s.data[keyHash]
    if !ok {
        return nil, false
    }
    return entry.value, true
}

The map itself is a standard Go map with pre-sized capacity. No custom hash table. Go's runtime map is already excellent when you don't force it to allocate.

Layer 2: Parser — Zero-Copy, Stack-Allocated Buffers

The parser reads directly from gnet's ring buffer. No intermediate []byte copies. Arguments are parsed as sub-slices of the input buffer:

// args point into the input buffer, no allocation
func (p *Parser) Parse() ([][]byte, error) {
    // Stack-allocated argument slice (capacity 8, covers most commands)
    var argsBuf [8][]byte
    args := argsBuf[:0]
    // ... parse RESP bulk strings as sub-slices ...
    return args, nil
}

For pipelined commands, we reuse the same args slice across iterations. The parser's ParseSetTTL uses unsafe.String + strconv.Atoi to parse integers without allocating a temporary string.

Layer 3: Network Layer — gnet Event Loop

Tellstone uses gnet for its event-driven network layer. No goroutine-per-connection. No net.Conn wrappers. Raw epoll/kqueue events drive the read/write path.

func (s *Server) OnTraffic(c gnet.Conn) gnet.Action {
    // Read directly from gnet's buffer, zero-copy
    buf := c.Peek(c.InboundBuffered())
    // Parse RESP in-place
    args, err := p.Parse(buf)
    // Dispatch, write response, reset buffer
    // ...
    return gnet.None
}

For TLS connections, we forked gnet-io/tls into internal/tls — The TLS layer also reports 0 allocs/op after optimization. This provides TLS 1.3 and works with gnet's epoll system BUT has the tradeoff that we have more complexity as it is basically a fork of crypto/tls -- We'll look forward to replacing it once gnet has built in tls support.

Layer 4: Protocol — Native Binary Protocol

Beyond RESP2, Tellstone has a native binary protocol for maximum throughput. Fixed-size headers, no parsing overhead:

[1B msgType][1B opCode][4B keyLen][key][4B valueLen][value]

Decode is 1.46 nanoseconds with zero allocations. The entire request-response cycle — network read, parse, execute, serialize, network write — completes in under 100 nanoseconds on bare metal.

Layer 5: Encryption — ChaCha20-Poly1305 In-Place

At-rest encryption uses ChaCha20-Poly1305, but we encrypt/decrypt in-place on the same buffer. No temporary copies:

func EncryptInPlace(key, nonce, plaintext []byte) ([]byte, error) {
    // Encrypts plaintext in-place, returns the same slice with AEAD tag appended
    aead.XORKeyStream(plaintext, plaintext)
    return plaintext, nil
}

190 nanoseconds for encrypt, 201 for decrypt. Zero allocations. The encryption cost is negligible compared to the network round-trip.

Layer 6: Persistence — WAL with Stack-Allocated Headers

The write-ahead log (WAL) uses a 16-byte stack-allocated header and zero-copy direct file writes per shard! 676 nanoseconds per WAL write. Zero allocations. The WAL is fsynced for durability but doesn't touch the heap.

WAL is not yet production ready. It works, but we'll look forward on our roadmap to implementing a robust WAL logic

The Scaling Story {#the-scaling-story}

Dragonfly demonstrates excellent multi-core scalability, but Tellstone's shared-nothing architecture continues to scale further under this workload.

Cores  Dragonfly       Tellstone      Tellstone lead
 4      1.4M ops/s      2.4M ops/s     1.74x 
 16     4.1M ops/s      6.8M ops/s     1.65x  
 32     7.3M ops/s      12.7M ops/s    1.75x 

Dragonfly's scaling is nearly linear (1.18x → 3.62x → 6.59x Redis). Tellstone's is more linear (2.06x → 5.98x → 11.53x Redis). The gap stays constant at ~1.75x because:

  1. No allocator contention. Tellstone uses Go's runtime allocator but never hits it on the hot path.
  2. No pointer chasing. Tellstone's shard-local maps use Go's runtime map (hash table with open addressing). At 32 cores, pointer chasing kills cache locality.
  3. GC is a non-issue. This sounds counterintuitive — Go has a GC, C++ doesn't. But when you have zero allocations, the GC never runs. It's a non-issue. Meanwhile, Dragonfly's custom allocator has its own overhead (thread-local caches, deferred frees, background threads).

The Cloud Reality: Network-Limited {#the-cloud-reality}

On bare metal, Tellstone is 12.7M ops/s. In the cloud (VM-to-VM via virtual SDN), the picture changes:

Scale               Tellstone    Redis      Dragonfly
Small  (4t/16c)     664K ops/s   636K ops/s  552K ops/s
Medium (16t/64c)    870K ops/s   831K ops/s  864K ops/s
Large  (60t/128c)   856K ops/s   849K ops/s  843K ops/s

The cloud provider's virtual switch imposes a ~850K ops/s PPS ceiling. At medium and large scale, everyone hits the same wall. Tellstone's advantage shows at small scale (4t/16c) where we're 20% faster than Dragonfly and 4.4% faster than Redis.

The latency story is more interesting. At medium scale (16t/64c):

Engine       p50       p99       p99.9
Tellstone    11.78ms   12.67ms   17.02ms
Dragonfly    10.05ms   40.96ms   62.98ms

Dragonfly has better p50 but 4x worse p99. Tellstone's tail latency is 3x better than Dragonfly's because there's no GC pause, no allocator stall, no cache miss cascade.

What We Learned {#what-we-learned}

  1. Zero allocations is a architecture decision, not an optimization. You can't bolt it on later. Every data structure, every buffer, every parse function must be designed from day one to avoid the heap.
  2. Go's map is fast enough. We didn't need a custom hash table. Go's built-in map proved sufficient for this workload, eliminating the need for a custom hash table.
  3. gnet changes everything. Goroutine-per-connection is dead for high-throughput servers. gnet's event loop with direct buffer access eliminates the overhead of net.Conn abstraction layers.
  4. The GC is your friend when you don't allocate. Go's garbage collector is designed for zero-allocation hot paths. When the heap never grows, the GC is a background thread that does nothing. It's the best of both worlds: memory safety without runtime cost.
  5. Bare metal numbers are marketing. Cloud numbers are reality. We publish both. On bare metal, we're 12.7M ops/s. In the cloud, we hit the same PPS ceiling as everyone else. The difference is tail latency — and that's where zero allocations shine.

What's Next {#whats-next}

Checkout the Roadmap

GitHub: github.com/Tellstone/Tellstone

Benchmark hardware: Intel Xeon Platinum 8580, 56 cores, 118 GB RAM, Debian. memtier_benchmark 1.3.0, 256-byte values, 1:10 read:write, pipeline 10. Cloud: VM-to-VM via virtual SDN, 100k requests. Benchmark done on STACKIT