To scale beyond one machine and avoid shared-state bugs, model the server as independent actors that own their state and communicate by messages. Then shard the world across many servers.
The actor model sidesteps locks entirely: an actor is a unit of computation that owns its private state and communicates only by sending and receiving messages, processing them one at a time. Because no state is shared, there are no races and no locks — concurrency safety by design. Each player, room, or entity can be an actor. This message-passing model (Erlang, Akka, Orleans) underpins many large-scale game and chat backends.
ACTOR: owns PRIVATE state, communicates ONLY by messages, processes them
ONE AT A TIME (a mailbox). No shared state -> no locks, no races. By design.
[Player A actor] --msg--> [Room actor] --msg--> [Player B actor]
owns A's state owns room state owns B's state
each actor's inbox is processed sequentially, so its own state is never
touched concurrently -> internal logic is simple, single-threaded reasoning.
many actors run in parallel across cores/machines.
Model each player / room / match / entity as an actor. Used by Erlang (WhatsApp,
game servers), Akka (JVM), Microsoft Orleans (Halo, many games), Elixir.The actor model is the practical form of "don’t share, communicate." Instead of many threads locking a shared world, each actor mutates only its own state and asks others to act by sending messages. This eliminates the lock-based hazards (races, deadlocks) at the cost of a different design style — you think in terms of messages and eventual responses rather than direct calls. For a game world of many independent entities, it fits naturally.
import asyncio
class PlayerActor:
def __init__(self, pid):
self.pid = pid
self.hp = 100 # PRIVATE state — only this actor touches it
self.inbox = asyncio.Queue()
async def run(self):
while True:
msg = await self.inbox.get() # process one message at a time
if msg["type"] == "damage":
self.hp -= msg["amount"] # safe: no one else touches self.hp
elif msg["type"] == "heal":
self.hp = min(100, self.hp + msg["amount"])
async def send(self, msg):
await self.inbox.put(msg) # others ASK by message, never reach in
# no locks anywhere: each actor's state is private + its inbox is sequential.
# "don't communicate by sharing memory; share memory by communicating."To scale past one machine, you partition — shard — the work across servers: different matches on different servers, or, for one large world, different zones each owned by a server, with actors migrating between them as players move. Because actors already communicate by messages and own their state, they distribute across machines naturally — a local message and a remote message look the same to your code. This is how a game backend grows from one server to a fleet.
Scale beyond one machine by PARTITIONING (sharding) the work:
many matches -> each match/lobby runs on some server in a fleet
one big world -> split into ZONES, each owned by a server; actors (players/
entities) MIGRATE between servers at zone boundaries.
the actor model helps: actors already own their state + talk by messages, so a
message to a LOCAL actor and a message to a REMOTE actor look the same to your
code -> the system distributes across machines with little change.
a directory/orchestrator (e.g. Orleans "virtual actors", Kubernetes + a
registry) tracks where each actor lives and routes messages.
This is distributed systems (Software Architecture course) applied to a live game world.A production game server layers these ideas: an event loop (async/await) handles the thousands of network connections cheaply, a thread or process pool runs CPU-heavy simulation in parallel, actors or careful synchronization keep shared game state safe, and sharding spreads load across a fleet. No single technique does it all — you combine them by matching each to the kind of work. That layered design is what lets an online game serve millions concurrently.
A real game server layers the concurrency tools by the WORK each does:
NETWORK I/O (many idle connections) -> event loop / async-await (one thread,
thousands of connections, cheap)
CPU-heavy sim (physics, pathfinding) -> thread / process pool (use all cores)
SHARED GAME STATE -> actors (no shared state) or careful locks
BEYOND ONE MACHINE -> shard across a server fleet
No single technique wins alone. Match each to the work:
async for I/O concurrency · threads/processes for CPU parallelism ·
actors/message-passing for safe shared state · sharding for horizontal scale.
That layered design is how an online game serves millions of concurrent players.