The Actor Model in Game Development
Understand the core benefits of the Actor Model for game developers: enhanced scalability, simplified concurrency, and improved reliability, crucial for delivering next-generation gaming experiences.
In a modern online game, especially an MMORPG, the hardest problem is rarely whether the server can use multiple CPU cores.
The harder question is:
Who is allowed to own and mutate a piece of state at any given moment?
A player is moving. A monster is selecting a target. A party is changing leaders. A trade is deducting currency from one side while transferring an item to the other. A player is crossing into another zone at exactly the moment their connection drops.
All of these are state transitions.
When a system is small, we can place that state inside a few objects, share them across threads, and protect them with mutexes, read-write locks, atomics, or carefully designed synchronization rules.
There is nothing inherently wrong with that. Many extremely fast systems are still built this way.
The problem appears when the number of state owners, lifecycles, and interactions grows large enough that the system can no longer answer basic questions clearly:
Which thread is allowed to modify this data?
At which points can this operation be interrupted?
What happens when two subsystems both believe they own the same state?
If a request times out, did the operation fail, or was only the response lost?
Does a command from an old connection remain valid after the player reconnects?
When a node dies, which state must be recovered and which state can simply be discarded?
The Actor Model is attractive because it offers a strong answer:
Each region of mutable state has one owner. To affect that state, send a message to its owner.
That is the most valuable idea in the Actor Model.
Not the mailbox.
Not the framework.
Not the promise of effortless distribution.
And certainly not the idea that every object in a game should become an actor.
Its real value is that it makes ownership explicit.
What an actor actually is
An actor usually contains three things:
Private state: only that actor may directly inspect and mutate it.
A mailbox: incoming messages wait here.
Behavior: the logic that processes those messages and decides the next state.
An actor handles messages sequentially. While it is processing one message, another actor cannot reach into its state and mutate it behind its back.
Consider an actor responsible for a guild:
InviteMember
AcceptInvitation
PromoteOfficer
RemoveMember
TransferLeadership
DisbandGuild
Instead of several request handlers concurrently modifying the same in-memory membership structure, every state-changing command goes through one owner.
While PromoteOfficer is being processed, another thread cannot simultaneously remove the same member from the actor’s internal state.
This sounds almost trivial. Yet this small rule removes an entire class of bugs:
race conditions inside the actor’s state;
lock-ordering problems;
deadlocks involving several mutexes;
state mutated by a subsystem that does not own it;
invariants that remain correct only because developers remembered to call methods in a particular order.
An actor turns a concurrent mutation problem into a sequence of state transitions:
Current state
+ Message
-> Next state
+ Outgoing effects
That is a powerful mental model.
But it needs to be stated precisely: actors do not remove concurrency from the system. They move concurrency from inside a state owner to between state owners.
Internal races may disappear, but they are replaced by a different family of problems:
delayed messages;
reordered messages;
duplicate delivery;
an actor crashing after committing state but before replying;
actors waiting on one another;
unbounded mailbox growth;
ownership changing while messages are still in flight.
The Actor Model does not make distributed systems simple.
It makes their difficult boundaries more visible.
That is still extremely valuable, but it is not the same promise.
“No shared mutable state” does not necessarily mean “no shared memory”
A common explanation of the Actor Model says that actors do not share memory.
At the semantic level, this is a useful rule. At the implementation level, it is not always literally true.
Two actors might run:
in different operating-system processes;
on different machines;
inside the same process;
on the same worker thread;
or across a shared scheduler and thread pool.
In an in-process runtime, a message may be passed using a cheap handle, index, or reference. It does not necessarily need to be serialized into bytes every time.
The important distinction is not whether both actors live in the same address space.
The important rule is:
One actor must not use a shared reference as permission to mutate state owned by another actor.
Actor isolation is an architectural contract. It is not automatically a physical barrier.
In Rust, the type system can help enforce that contract. In C++ or C#, more of the burden falls on framework design and programmer discipline. An actor runtime inside one native process will not save the system from memory corruption, undefined behavior, global allocator failure, or a blocking call that starves the scheduler.
It helps to separate three different forms of isolation.
Logical isolation
Only the designated owner may mutate the state.
This is what the Actor Model provides best.
Scheduling isolation
Can one slow or blocked actor prevent unrelated actors from making progress?
That depends on the runtime and scheduler.
Failure isolation
Can one actor crash the entire process?
That depends on whether the actor lives in its own process, a managed runtime, or a native process with shared fate.
These should not be collapsed into a vague claim that actors are automatically fault-tolerant.
Actors work best when state has identity and lifecycle
The Actor Model is particularly natural when state has three properties:
It has a stable identity.
It protects meaningful invariants.
It changes mainly through discrete commands rather than continuous bulk computation.
Good examples include:
PlayerAccount
Guild
Party
Match
AuctionListing
ChatRoom
DungeonInstance
ZoneCoordinator
A guild has an identity. It owns members, roles, invitations, leadership, and rules such as:
there can be only one leader;
a non-member cannot be promoted;
a disbanded guild cannot accept new members;
an expired invitation cannot be accepted;
leadership cannot be transferred to an ineligible member.
An actor is a natural place to protect those rules.
A message arrives. The actor checks its current state, decides whether the command is valid, updates the state, and produces a response or event.
PromoteMember {
guild_id,
requester_id,
target_id,
expected_version
}
The command can be evaluated inside one logical critical section without exposing the guild’s mutable state to outside callers.
Now compare that with updating the positions of 50,000 entities:
position += velocity * delta_time
The data is homogeneous. The work is repetitive. The goal is to process it in dense batches with good cache locality, predictable memory access, and perhaps SIMD.
Turning 50,000 entities into actors and sending each one a Tick message can transform a cheap calculation into:
50,000 mailbox operations;
50,000 scheduling decisions;
pointer chasing;
poor locality;
metadata for every actor;
more complicated tracing and lifecycle management.
That is not what actors are naturally good at.
An ECS or another data-oriented simulation loop is usually a much better fit.
A useful rule is:
Actors fit stateful authority with identity.
Data-oriented loops fit large-scale homogeneous computation.
Actors and ECS are not competing religions
Actor Model and ECS are often presented as two alternative architectures.
In practice, they usually operate at different levels.
ECS answers:
How do we process large numbers of similarly structured entities efficiently?
Actors answer:
Who owns a region of state, and how are competing commands serialized at that boundary?
A sensible game-server architecture can use actor-like ownership at the boundary without creating an actor for every entity.
For example, a zone runtime may behave like one coarse-grained actor:
ZoneRuntime
├── accepts admitted player commands
├── owns the simulation lifecycle
├── defines tick boundaries
├── handles join, leave, and handoff
└── produces replication output
Inside that zone, the simulation can still use:
dense arrays;
ECS archetypes;
preallocated command buffers;
spatial grids;
batched collision;
fixed-step deterministic processing.
The zone has an inbox. The individual monsters do not each need one.
This is also close to how I have found actor thinking useful while working on an MMORPG architecture. I do not treat it as a doctrine that should cover the whole runtime. I use it mainly to reason about ownership, admission, and lifecycle boundaries. The hot paths—simulation, visibility processing, replication, and similar workloads—still need dense data, bounded work, and strong locality.
In simpler terms:
Actors at control boundaries.
Batch processing in the data plane.
That combination is usually healthier than trying to make everything an actor.
“One actor per player” is more complicated than it sounds
The idea is immediately attractive.
A player has state. A player sends commands. A player needs isolation. Therefore, create one actor for every player.
The problem is that “the player” is not one coherent region of state in an MMORPG.
A player may simultaneously exist as:
a durable account identity;
an authenticated session;
a character progression record;
an inventory owner;
a social presence;
an avatar inside a zone;
a network connection;
a replication cursor;
a participant in marketplace operations.
If all of that is placed inside one PlayerActor, the actor quickly becomes a distributed god object.
It may be expected to process:
Move
CastSkill
EquipItem
ReceiveMail
JoinGuild
BuyAuction
ChangeZone
Reconnect
Logout
SaveProgress
Every subsystem then has to communicate through it. The mailbox becomes a general-purpose traffic intersection. The latency of one category of command may be affected by unrelated work. Ownership between the world simulation and durable account state becomes increasingly vague.
A cleaner design may split the state according to its invariants and lifecycle:
Account owner
Character persistence owner
Session-incarnation owner
Zone-local avatar authority
Inventory aggregate
Guild or social aggregate
But this decomposition should not be copied mechanically either.
Every new actor boundary creates more:
protocol surface;
versioning;
failure modes;
ordering questions;
tracing burden;
recovery policy.
Actor decomposition is not free.
A separate actor is justified only when the boundary represents genuinely independent ownership, lifecycle, placement, or failure handling.
How the Actor Model can make a system worse
Most introductions to actors spend nearly all their time explaining what actors solve.
The more important engineering question is what they can damage.
Too many actors, too little architecture
Once a team begins treating every noun as an actor, the system can rapidly devolve into this:
PlayerActor
ItemActor
QuestActor
BuffActor
SkillActor
ProjectileActor
DoorActor
ChestActor
A simple action such as opening a chest may become:
PlayerActor -> ChestActor
ChestActor -> LootTableActor
LootTableActor -> InventoryActor
InventoryActor -> CapacityPolicyActor
CapacityPolicyActor -> InventoryActor
InventoryActor -> PlayerActor
On an architecture diagram, this looks decoupled.
In production, it is a call stack torn into several queues. There is no natural stack trace, no obvious transaction boundary, and each hop introduces another timeout, retry rule, ordering assumption, and failure point.
Decoupling is not always a virtue.
State that changes together often belongs under the same owner.
Mailboxes can hide overload
Lock contention is usually visible. Threads block, profilers show hot locks, and latency spikes around recognizable synchronization points.
Mailbox overload can be more deceptive because the system continues to appear alive.
Messages are still being accepted. The queue grows. Latency slowly increases. Memory usage climbs. Replies become increasingly stale. Eventually, the process falls over under a backlog whose root cause appeared several minutes earlier.
An unbounded mailbox is effectively debt with no repayment schedule.
Every production actor needs explicit answers to questions such as:
What is the mailbox capacity?
What happens when it is full?
Are messages rejected, dropped, coalesced, or backpressured?
Which messages have priority?
Which messages become worthless when delayed?
Can several messages be processed as a batch?
How much CPU time may one actor consume before yielding?
How is a hot actor partitioned?
Without these answers, the mailbox is simply a place to hide queueing problems.
Asynchrony destroys convenient transaction boundaries
In synchronous code, an operation may appear as:
validate
reserve item
deduct gold
transfer item
commit
Once the responsibility is split across actors, each line may become a separate message.
Now the design must handle cases such as:
gold was deducted but item transfer timed out;
item transfer succeeded but the response was lost;
a retried command executes twice;
an actor restarts halfway through the workflow;
compensation begins and then compensation also fails.
Actors do not replace transaction semantics.
They force us to model those semantics explicitly through mechanisms such as:
idempotency keys;
versions or fencing tokens;
reservations;
sagas;
durable events;
explicit commit states;
reconciliation jobs.
That explicitness can improve the system. It does not make the system simpler.
ask() can become a remote function call in disguise
Many actor-based codebases eventually write something equivalent to:
await actor.ask(message)
for nearly every interaction.
At that point, the system still has logically synchronous call chains. Each function call has merely acquired a mailbox, scheduler hop, serialization boundary, and timeout.
If Actor A waits for B, B waits for C, and C needs A, the system has created a protocol-level distributed deadlock without using a single mutex.
Actors provide the most value when flows are designed as state transitions and event progression. When mailboxes are used only to simulate remote object methods, the system absorbs most of the cost while receiving little of the benefit.
Actors cannot rescue a badly chosen hot path
An actor processes messages sequentially. That is valuable for correctness, but it also places a hard limit on throughput.
If every player command goes through one WorldActor, that actor is a global lock with different branding.
If all marketplace activity passes through one AuctionHouseActor, the actor becomes hot.
If a massive battle is owned by one zone actor, adding another hundred nodes to the cluster does not automatically divide that actor’s workload.
The Actor Model helps partition state.
It does not discover the correct partition key for you.
“Let it crash” is less magical than it sounds
The Erlang/OTP idea of “let it crash” is often repeated without the conditions that make it work.
It does not mean:
Write careless code and restart whatever breaks.
It depends on strong assumptions:
processes are cheap;
critical state can be reconstructed or restored;
side effects have clear semantics;
supervisors know which failures are safe to restart;
restart loops are bounded;
each process has a limited blast radius;
runtime isolation is strong enough to contain the fault.
Restarting a session actor after receiving a malformed network packet may be reasonable.
Restarting an actor halfway through a trade does not automatically make the trade correct.
Restarting a simulation actor also does not recover several seconds of authoritative world state by magic.
Before relying on supervision, state needs to be classified.
Ephemeral state
This can be discarded and reconstructed.
Examples include:
cached views;
temporary session projections;
transient path requests;
replication scratch buffers.
Recoverable state
This can be restored from a snapshot, event log, or durable record.
Examples include:
guild state;
match lifecycle;
durable workflows.
Authoritative volatile state
This cannot be cheaply reconstructed, but it also cannot realistically be persisted on every tick.
Examples include:
active combat state inside a zone;
cooldowns and projectile state;
a live encounter with many interacting entities.
The third category is where MMORPG architecture becomes difficult.
“Restart the actor” is not enough. The system may need checkpoints, handoff protocols, degraded recovery, state replication, or an explicitly accepted failure domain.
Supervision is a recovery policy.
It is not resurrection.
Process actors, in-process actors, and virtual actors
Not every actor system has the same cost model or guarantees.
Process-style actors
Each actor, or group of actors, lives inside a relatively independent failure domain.
Advantages:
strong isolation;
good crash containment;
suitable for coarse-grained services.
Costs:
IPC or network communication;
serialization;
higher memory overhead;
poor fit for high-frequency per-tick workloads.
In-process actors
Many actors run inside one process using a shared scheduler.
Advantages:
cheaper messages;
lightweight actors;
efficient scaling within a node.
Costs:
shared process fate;
dependence on scheduler quality;
blocking code can create starvation;
memory corruption can take down the entire runtime.
Virtual actors
A virtual actor is addressed by logical identity. The runtime activates, deactivates, and places it across a cluster.
This can be extremely useful for:
accounts;
player profiles;
guilds;
lobbies;
matchmaking;
durable application state.
But virtual actors can create the illusion that distribution has been solved.
Location transparency does not make the network disappear.
A call to an actor on another node still has:
latency;
partial failure;
serialization;
retry behavior;
version skew;
load imbalance.
Hiding topology from business code may improve productivity. Architects and operators still need to understand that topology.
When I would seriously consider actors
Actors are a strong candidate when the state has:
a natural owner;
stable identity;
important invariants;
several competing command sources;
event-driven rather than per-tick behavior;
independent placement or lifecycle needs;
meaningful failure-isolation requirements;
a message boundary that also represents a real domain boundary.
Examples include:
Guild
Party coordinator
Match lifecycle
Dungeon-instance coordinator
Player account session
Marketplace listing
Chat room
Zone admission
Cross-zone transfer workflow
When I would avoid actors
I would generally avoid per-entity actors for:
transform updates;
physics integration;
spatial queries;
AOI scanning;
replication-diff generation;
projectile simulation;
animation processing;
bulk stat recomputation;
homogeneous calculations that can be processed in batches.
I would also avoid introducing actors when an object is already owned by one thread, has no independent lifecycle, and has no need for distribution.
In that situation, an actor may only add a mailbox to something that was already single-owner.
A plain struct and a clear loop may be the better architecture.
Questions worth asking before creating an actor
Before turning something into an actor, I would ask:
Who actually owns this state?
If the answer is unclear, the actor may only be hiding ambiguous ownership.
Which invariant does this actor protect?
If there is no concrete invariant, it may simply be a service object with a queue.
Why does it need a mailbox?
Are there genuinely concurrent producers, or is message passing being introduced because the framework makes it convenient?
Is its workload bounded?
What happens when producers are faster than the actor?
What happens if it dies during a side effect?
Is a restart safe? Is the command idempotent? Can the effect be reconciled?
Is this control-plane work or data-plane work?
If it is a dense hot path, actor-per-entity is usually a warning sign.
Is this boundary worth creating another protocol?
A new actor also means new messages, observability, failure semantics, compatibility rules, and operational burden.
If the answers remain vague, the design is not ready for another actor.
The point is ownership, not actors everywhere
The Actor Model is not valuable because it is fashionable, distributed, or allegedly free of locks.
It is valuable because it forces a system to answer:
Who owns this state, where do commands enter, and at which boundary are mutations serialized?
That is an excellent foundation for the parts of a game server that contain strong identity, complex invariants, and independent lifecycles.
But the model should not leak into every entity and every update loop.
In the data plane, locality, batching, bounded memory, and predictable cost usually matter more than an elegant messaging abstraction. A well-designed ECS or monomorphic simulation pipeline can outperform a web of tiny actors not only in raw speed, but also in comprehensibility.
My own experience applying these ideas to an MMORPG has been that the most useful lesson was never “use an actor framework everywhere.”
It was learning to recognize the real owner.
Some boundaries should behave like actors.
Some should be aggregates.
Some hot paths should remain nothing more than dense data moving through a fixed pipeline.
The Actor Model is therefore not the architecture of the whole system.
It is a tool for cutting the system along the correct ownership lines.
Cut it well, and the system becomes dramatically easier to reason about.
Cut it badly, and the result is merely thousands of mailboxes sending problems to one another.
Comments