12.7 million ops/s In-Memory Database in Go — Here's How We Hit Zero Allocations
TL;DR
- Result: Tellstone processes 12.7 million ops/s on a 32-core Intel Xeon 8580 — 11.5x Redis, 1.75x Dragonfly.
- Key Technique: Zero allocations on the hot path via pre-hashed sharded maps, stack-allocated RESP parsing, gnet event loop, in-place encryption, and WAL with stack-allocated headers.
- Cloud Reality: In cloud VM-to-VM tests, all engines hit the same ~850K PPS network ceiling. Tellstone's advantage is tail latency — 3x better p99 than Dragonfly.
Table of Contents
- The Benchmark That Started Everything
- Why Zero Allocations Matter
- How We Achieved It: The Stack
- The Scaling Story
- The Cloud Reality
- What We Learned
- What's Next
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:
- Heap growth → more memory pages to manage
- GC pressure → the garbage collector wakes up, stops the world (or does concurrent marking), scans pointers
- Cache pollution → allocated objects land in new memory, cache lines miss
- 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:
- No allocator contention. Tellstone uses Go's runtime allocator but never hits it on the hot path.
- 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.
- 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}
- 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.
- 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.
- 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.Connabstraction layers. - 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.
- 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}
- TLS 1.3 — PR opened
- AUTH + RBAC — role-based access control with zero-allocation permission checks
- Vector Search — approximate nearest neighbor with HNSW indexing
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