Persistent multiplayer state without chaos

der_gopher1 pts0 comments

Persistent multiplayer state without chaos

SubscribeSign in

Persistent multiplayer state without chaos<br>How I keep a live multiplayer game consistent with PostgreSQL holding the truth and Redis doing the fast work

Julien Singler<br>May 25, 2026

Share

In a single-player game, state is easy. There’s one player, one save file, and as long as you don’t corrupt the file, you’re fine.<br>In a live multiplayer game, state is the hardest problem you’ll have. Two players can act on the same target at the same time. A scheduled job (a build, a launder, a raid) needs to resolve at exactly the right moment regardless of who’s online. A player can come back after three days and expect their progress to be where they left it, plus whatever happened in the world while they were gone.<br>You can’t naively keep this in memory. You can’t lazily flush to disk on logout. You need a real persistence story, and you need it to handle concurrent access without melting.<br>Here’s the architecture I landed on.<br>The problem: two kinds of state, one source of truth

A multiplayer game has two flavors of state:<br>1. Authoritative state. Player resources, owned nodes, queued actions, completed achievements. If this is wrong, the game is broken. Players notice within seconds and rage-quit.<br>2. Hot state. Things you read constantly but recompute often: target defense snapshots, online presence, active raid timers, leaderboard pages. If this is slightly stale, no one cares.<br>Trying to put both in the same store is what breaks games. Either you put hot state in PostgreSQL and your hot path is one query per click, or you put authoritative state in Redis and one OOM kills your economy.<br>The fix is to be honest about which is which.<br>Sponsor

Agent loop is the most important piece of infrastructure in your workflow right now and for most developers, it’s the one piece they can’t open up. Agent builders have to jump through all the hoops themselves, crafting the infrastructure and tools, testing the harness, while fighting to maintain what they’ve built.<br>Meet Cline SDK : agent harness behind Cline 2.0, fully open-sourced. The same runtime that powers Cline across VS Code, JetBrains, and the CLI is now an npm install away: npm i @cline/sdk . Inspect it, fork it, extend it, ship on it.<br>Best-in-class harness: 74.2% on Terminal-Bench 2.0 with Claude Opus 4.7 ahead of Claude Code (69.4%) and strongest numbers published on open-weight models.

Open model & provider choice: Anthropic, OpenAI, Google, Bedrock, Mistral, or any OpenAI-compatible endpoint.

Real plugin system: Register tools, hooks, commands, providers, message builders. Prototype as a local file, harden into a package. Extend it freely for any of your agent use cases.

Scheduled + event-driven agents: Cron and event specs for PR reviews, dependency checks, coverage audits, changelogs no separate orchestration layer.

Stop building around your agent. Start building on it.<br># Install CLI:<br>npm i -g cline

# Install SDK<br>npm install @cline/sdk

Get Started Today<br>The design choice: PostgreSQL is truth, Redis is speed

The rule I follow:<br>- PostgreSQL holds the source of truth. Player rows, node rows, queued jobs, transactions. Every authoritative mutation goes through a transaction.<br>- Redis holds derived or hot state. Cached node lists, defense snapshots, presence, locks for periodic jobs. Anything in Redis can be rebuilt from PostgreSQL.<br>If Redis disappears tomorrow, the game is slow but correct. If PostgreSQL disappears tomorrow, the game is gone. That asymmetry tells you exactly what each store is for.<br>The implementation: transactions, repositories, and a thin cache layer

The backend is Go (Echo + pgx). The structure is the standard 3-layer:<br>api/ — Echo handlers, mostly thin

game/ — services that orchestrate game logic

db/ — repositories with explicit SQL

Every authoritative mutation is wrapped in a transaction. Reads of hot data first hit Redis, fall through to PostgreSQL on miss, and the cache is invalidated whenever the underlying row is written.<br>A single Redis pattern that pays for itself many times over: distributed locks for periodic jobs. A live game runs background tickers — heat decay, build completion, passive income, raid resolution. If you scale to two backend instances, every one of those tickers will fire on every instance and double-apply. The fix is a short-lived Redis lock keyed by job name:<br>ok, err := s.redis.GetClient().SetNX(ctx, "heat_decay:lock", "1", s.interval).Result()<br>if err != nil || !ok {<br>// Another instance holds the lock — skip this tick.<br>return<br>affected, err := s.players.DecreaseAllHeat(ctx, heatDecayPerTick)

SetNX with a TTL equal to the tick interval guarantees exactly one instance applies the decay per window, and if the holder crashes the lock expires automatically. No leader election, no Zookeeper, no surprise.<br>For per-player mutations, the lock is just a SELECT ... FOR UPDATE inside a transaction. PostgreSQL is good at this and you do not need to invent a lock...

state game redis postgresql multiplayer player

Related Articles