Skip to main content

Actor Model

The Actor Model is a mathematical model of concurrent computation that treats "actors" as the universal primitives of a system. An actor is an independent computational entity that communicates with other actors exclusively by exchanging messages. Each actor has a private state, which it can modify only in response to a message, and a mailbox to buffer incoming messages. This model provides a powerful foundation for building highly concurrent, distributed, and fault-tolerant systems.

"Everything is an actor. An actor is a computational entity that, in response to a message it receives, can concurrently: send a finite number of messages to other actors; create a finite number of new actors; designate the behavior to be used for the next message it receives." — Carl Hewitt

Actor System Hierarchy: A supervisor manages worker actors, delegating tasks and handling failures.

Core ideas

  • Isolation (No Shared State): Actors do not share memory. The internal state of an actor is completely encapsulated and can only be modified by the actor itself. This eliminates the need for locks and other complex synchronization mechanisms.
  • Asynchronous Messaging: Communication is done by sending immutable messages to an actor's unique address. Message passing is asynchronous and non-blocking.
  • Mailbox: Each actor has a mailbox (a queue) where incoming messages are stored until the actor is ready to process them. This provides a natural mechanism for backpressure.
  • Behavior: An actor processes one message at a time, and can change its behavior for the next message it processes. This allows for stateful, finite-state-machine-like logic.
  • Supervision: Actors form hierarchies. A parent actor can supervise its children, deciding how to handle their failures (e.g., restart, stop, or escalate). This "let it crash" philosophy is key to building resilient systems.

Examples

These examples demonstrate the basic concept of an actor as an isolated process with a message queue, not a full-fledged, fault-tolerant implementation like Akka or Erlang/OTP.

Sequential call flow for the counter actor example.
actor.py
import asyncio

class CounterActor:
def __init__(self):
self._count = 0
self._mailbox = asyncio.Queue()

async def run(self):
"""The actor's main processing loop."""
while True:
message = await self._mailbox.get()
if message == "get":
print(f"Count is: {self._count}")
elif isinstance(message, int):
self._count += message
self._mailbox.task_done()

def send(self, message):
"""Send a message to the actor's mailbox."""
self._mailbox.put_nowait(message)

async def main():
actor = CounterActor()
# Start the actor's processing loop in the background
asyncio.create_task(actor.run())

actor.send(1)
actor.send(1)
actor.send("get")
await asyncio.sleep(0.1) # Allow time for processing

if __name__ == "__main__":
asyncio.run(main())
When to Use vs. When to Reconsider
When to Use
  1. Highly concurrent systems: Ideal for applications with thousands or millions of concurrent activities, like chat servers, IoT platforms, or gaming backends.
  2. Fault-tolerant, resilient applications: The supervision hierarchy allows for robust, self-healing systems that can gracefully handle failures.
  3. Distributed state management: When you need to manage distributed, mutable state without the complexity of distributed locks or transactions.
When to Reconsider
  1. Simple, synchronous request/response: The overhead of actors and message passing is unnecessary for simple CRUD applications.
  2. CPU-bound, parallelizable tasks: For tasks that can be parallelized over a dataset (like image processing), dataflow or fork-join models are often a better fit.
  3. Systems requiring strong consistency: While patterns exist, achieving strong consistency across actors can be complex compared to traditional database transactions.

Operational Considerations

A key feature of mature actor systems. An actor's address is logical, allowing it to be on the same machine or a different one without changing the code.
Mailbox size is a critical backpressure mechanism. Bounded mailboxes prevent memory exhaustion but require a strategy for dropped or rejected messages.
Define clear supervision strategies: should a failing actor be restarted? Should the failure be escalated to the parent? This is central to the system's resilience.
In a distributed system, messages must be serialized to be sent over the network. Choose an efficient and version-tolerant format.
Monitoring actor systems requires visibility into message queues (mailbox depth), processing times, and error rates. Use correlation IDs to trace a single request across multiple actors. Distributed tracing is essential for understanding system behavior.
Since actors encapsulate state and behavior, they can serve as security boundaries. However, in a distributed environment, messages sent over the network must be secured through encryption (TLS). Access control may be needed to ensure only authorized actors can send specific messages.

Design Review Checklist

  • Is state truly isolated within actors? Is there any 'backdoor' access to shared mutable state?
  • Are all messages immutable?
  • Is the supervision hierarchy clearly defined and tested for various failure scenarios?
  • Is the mailbox strategy (bounded/unbounded, priority) appropriate for the workload?
  • How are message delivery guarantees (at-most-once, at-least-once) handled if required?
  • Is there a plan for observing and debugging actor interactions in a distributed environment?

References

  1. Introduction to Akka (A popular Actor System toolkit) ↗️

  2. Erlang/OTP Design Principles ↗️

  3. The Actor Model in 10 Minutes by Brian Storti ↗️

  4. Hewitt, Meijer and Szyperski: The Actor Model (everything you wanted to know, but were afraid to ask) - A conversation with Carl Hewitt, Erik Meijer, and Clemens Szyperski on the history and impact of the Actor model. Available on YouTube and other platforms.