Aug 27, 2026 / 3 min read

Running a 20Hz game world on serverless functions

Server racks in a dark room, orange and green fibre runs lit across the cabinets

I have been building a small multiplayer game called Roamadic, a low poly forest park you walk around in with other people, and the part worth writing about is not the game, it is the backend. The whole thing runs on Vercel functions and one Redis database. There is no game server, no container, no VPS, nothing that stays on when nobody is playing.

That should not work. A real multiplayer game needs an authoritative simulation, one process stepping the world at a fixed rate and telling every client what is true, and serverless functions are built to die. Vercel force closes a WebSocket function after its max duration, instances get killed without warning, and deploys split traffic between old and new code. The usual answer is to give up and rent a pinned instance from somebody.

The shape I landed on has three pieces. Every player's WebSocket lands on its own relay function, which is deliberately dumb, it decodes inputs and publishes them to a Redis pub/sub channel and forwards snapshots back the other way. One ticker function per room runs the actual simulation at 20Hz, a fixed 50ms step. Redis is the only coordination between any of them.

The ticker is the interesting part. It holds a lease, a Redis key with a 5 second TTL that it renews every 1.5 seconds, and it checkpoints the entire world state to Redis once a second. It exits on its own at 700 seconds, before the platform would kill it, and its last act is spawning its own successor, which restores from the checkpoint and takes over in under a second. Players notice nothing, because the client prediction and interpolation that any decent netcode already needs was built to smooth over more disruption than that. If the ticker dies uncleanly instead, the relays notice the missing lease within a second or two and spawn a replacement themselves.

The part that took the longest to get right is guaranteeing exactly one ticker ever publishes. A lease TTL guarantees one holder, not one publisher, a ticker whose renews are silently failing will happily keep publishing next to its successor. The fix is two separate timestamps, one pacing renew attempts and one advanced only by a confirmed renew, and a check before every publish that the confirmed one is fresh. I got that wrong twice before it held, both times by letting a failed renew refresh the wrong clock.

As far as I can tell nobody ships this as a library. Everything else that does stateful realtime on serverless reaches for a pinned instance instead, Durable Objects and PartyKit are Cloudflare only, Rivet tunnels your sockets through its own gateway, and Colyseus wants a Node process that never dies. This design needs any FaaS that can hold a WebSocket and any Redis, and that is the whole list.

The economics are also better than they look. A snapshot publish is one Redis command no matter how many players are subscribed, so fan out is free where it matters. A full 20 player room costs me about $30 a month on a fixed Upstash plan. I priced the same room on services that bill per message delivered and it came out between $2,400 and $5,200 a month, which is the difference between a hobby and a funding round.

I will probably pull this transport layer out into its own open source package. The seam already exists in the code, the simulation is pure TypeScript with no Redis or socket imports and all the IO lives at the edges, so the lease, the checkpoints, and the handoff could ship as the library while the game stays mine. If you want realtime on Vercel without adding a second vendor, that is the thing I would want to exist.

Roamadic