How I keep a multiplayer browser city alive with zero players online
I'm a solo dev from Uruguay building Balak City, a multiplayer open-world city that runs in the browser: one shared city where everyone walks, drives, runs missions and buys businesses at the same time. It's plain JavaSc
I'm a solo dev from Uruguay building Balak City, a multiplayer open-world city that runs in the browser: one shared city where everyone walks, drives, runs missions and buys businesses at the same time. It's plain JavaScript and Canvas 2D, no engine.
The question that shaped the whole architecture was simple: what happens to the city when nobody is online?
The problem
In a lot of browser multiplayer games, the first player to join a room becomes the "host" and simulates the world for everyone. That works until:
- the host is on a cheap phone and the whole city stutters for everyone,
- the host switches tabs and the browser throttles their timers,
- the host leaves and the world freezes or resets.
For a city that's supposed to feel alive (traffic, pedestrians, police, factories producing goods), none of that was acceptable.
The solution: a headless player that never leaves
The city is simulated by a headless copy of the game itself, running on a small VPS:
- The game client is a single HTML page. The server runs that same page in a headless browser with a special key in the URL that tells it "you are the host".
- That page simulates cars, pedestrians, police and the economy, and broadcasts the state to the room.
- Real players connect to rooms on Cloudflare Workers + Durable Objects, which relay state between the host and everyone else.
- A tiny systemd service keeps the headless page alive and restarts it if it crashes.
The nice part: there is only one codebase. The host is literally the game, so every gameplay change automatically applies to the simulation too. To ship new code to the host without SSH, there's an authenticated endpoint that tells the headless page to reload.
Bugs I didn't expect
Two hosts fighting over the same city. During a deploy, the old host and the new one were briefly alive together, both broadcasting different versions of reality. Cars teleported, missions duplicated. The fix was making the host role exclusive at the room level, so a second host is ignored until the first one is gone.
Leaks that only show up after days. Bikes and cars spawned for missions far away from any player were never cleaned up. With real players this never mattered because sessions are short; a host that runs for a week accumulated thousands of them. Every spawned object now has an owner and an expiry.
Hidden tabs. When a player minimized the game, the host removed their character after 15 seconds of silence, so coming back put them somewhere else. The fix was to separate "this player is hidden" from "this player is gone".
Measuring smoothness from real players
Each client sends a small performance summary every 20 seconds (FPS, frame time percentiles, worst frame, ping, data arrival gaps, reconnects). With two players in different countries you can compare their timelines: if both drop at the same moment, it's the server or the game; if only one drops, it's that player's device. That simple correlation has saved me hours of guessing.
The renders
The images in this post are Blender renders generated from the game's map data with Python scripts (roads, lots, buildings, cranes, the stadium, the airport). In the game you see the same city top-down. It started as a way to make promo images, but it also exposed layout mistakes I never noticed from above.
Try it
It's free and runs on PC and phones: https://balakapp.win/?utm_source=devto
I'd love feedback on performance on your device, and I'm happy to answer questions about the host setup in the comments.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.
