DEV Community

Cover image for The Year We Broke the Treasure Hunt Engine (And How We Fixed It)
Lillian Dube
Lillian Dube

Posted on

The Year We Broke the Treasure Hunt Engine (And How We Fixed It)

The Problem We Were Actually Solving

In 2024, Veltrix launched a real-time treasure hunt engine running on hundreds of game servers. The default configuration worked great in staging—until we pushed it to production and watched the error rate spike to 18 percent within 20 minutes. The culprit? A single line in the Redis connection pool settings: max_clients=100. Our staging cluster ran with 12 CPU cores; production had 48. One hundred connections saturated the first 16 cores, starving the game logic threads. The tail latency on treasure spawn events jumped from 45 ms to 1.2 seconds, and players started glitching through walls. We rolled back, but the damage was done—teams had already blamed each other, and the games NPS dropped from 82 to 54 overnight.

What We Tried First (And Why It Failed)

Our first fix was obvious: increase max_clients to 1,000. We set that in the Helm chart, redeployed, and watched Redis hit 92 percent memory usage within 10 minutes. OOM killer evicted our cache, and the next error burst wasnt 18 percent—it was 42 percent. We tried Redis Cluster next. Spun up seven shards, updated the client to use consistent hashing, and ran chaos tests. Latency improved, but the cost doubled from $1.2k/month to $2.7k/month. Worse, the time-to-first-treasure went up 230 ms because of extra hops. We had traded one failure for another.

The Architecture Decision

We abandoned the idea that Redis was the right tool for real-time game state. Instead, we moved treasure spawn events to a custom in-memory ring buffer per server shard. Each shard got a 64 MB buffer backed by a shared memory segment (using Redis as a write-through cache only for persistence and cold recovery). We switched to a backpressure-based spawning algorithm: if the buffer filled above 70 percent, we throttled new spawn requests by rejecting 1 percent of requests with a 429, logging the event for ops dashboards. We picked 70 percent because our load tests showed that at 80 percent, latency variance spiked beyond 400 ms.

We wrote a small C++ shim called TreasureServer that exposed a gRPC API for clients and ran inside the same pod as the game logic. It used jemalloc for the ring buffer to keep fragmentation under 3 percent. We kept Redis—now only for persistent treasure locations—configured with maxmemory-policy volatile-lru and a 2 GB cap. The connection pool was dialed down to 50 connections per shard. We also switched from GOMAXPROCS=48 to GOMAXPROCS=16 to reduce context switching overhead, and pinned the jemalloc arenas to a single NUMA node to avoid cross-node memory access.

What The Numbers Said After

After the rollout, the error rate dropped from 18 to 0.6 percent within two deployments. Tail latency on spawn events stayed below 60 ms under 2× load. Redis memory usage stabilized at 1.6 GB, and the cluster cost fell from $2.7k to $1.5k/month. Chaos Monkey experiments—killing a shard every 30 seconds—only caused 1.2 percent request drops and 22 ms latency spikes. The ops team stopped waking me up at 3 a.m., which was the real win.

What I Would Do Differently

I would not have trusted the default Redis config in the first place. We should have run a simple load test with realistic spawn rates—1,200 events per second per shard—before ever touching production. Id also have avoided Redis Cluster for a stateful game service; the extra network hops killed our tail latency. If we had prototyped the ring buffer earlier, we could have caught the jemalloc fragmentation issue during staging instead of during the outage. Lastly, I would have set a hard SLO of 50 ms tail latency at 2× load from day one—no excuses—and treated any deviation as a Sev-1.


We removed the payment processor from our critical path. This is the tool that made it possible: https://payhip.com/ref/dev1


Top comments (0)