The Actor Model

2025-07-07
James Diacono
james@diacono.com.au

The actor model was proposed by Carl Hewitt in the 1970s. It has not received the attention paid to more influential models of computation, such as the Turing machine or lambda calculus, yet it is a better fit for today’s world.

I want to tell you about the actor model and why I think it is important. Let us begin on familiar ground.

Objects

Alan Kay coined the term “object-oriented programming” in the 1960s. In studying biology he had learned that cells cooperate to perform larger tasks by sending each other chemical messages. He realized that, similarly, software could be constructed out of many little virtual computers, which he called “objects”, and that these objects could cooperate by sending each other messages.

For practical reasons, Alan Kay’s team implemented the messaging between objects as method calls. This was such a popular choice that it has come to overshadow the biological origins of the idea. The advantage of method calls is that it is very easy to write code such that one thing happens after another. The disadvantage of method calls is that when an object calls the method of another object it must wait, frozen in time, until the method returns. A disturbing implication of this is that no more than one object is making progress at any given time.

When Carl Hewitt attended a talk by Alan Kay on object-oriented programming, he liked the cell analogy but felt that method calls were a mistake. After all, it would be absurd for a cell to stop doing anything until it heard back from another cell. What if the reply never came, or took a long time? He proposed a modification to Alan Kay’s idea, whereby communication would be more like mail than telephone. Carl Hewitt’s actors, unlike Alan Kay’s objects, send off their messages and keep rolling.

Messages

You have probably found that, at times, it is more convenient to exchange messages with people than to call them on the phone. Why is this so? For one, calling requires that both parties be ready to talk at the same time. If either party is unavailable, the call must be postponed. If the call is successfully established, there is the problem that both parties are engaged for the duration of the call. When you are engaged, you are unable to communicate with others and often unable to carry out tasks. In contrast, messages addressed to you will patiently wait in your inbox until you get around to reading them. When and how, or even if, you choose to act on a message is entirely up to you.

In short, messaging is convenient because it affords individuals greater freedom with their time than they can get by calling.

Concurrency

Imperative and object-oriented programming are inherently sequential. One thing happens after another, and no two things happen at the same time. These paradigms were conceived at a time when all programs ran on a single processor within a single computer. Sequentiality was a perfectly reasonable simplification that made programming easier and more predictable.

Until, that is, we started writing programs that ran across many computers, each with many processor cores. Valiant attempts were made to accommodate concurrency in sequential programming paradigms, usually by bolting on mechanisms such as threads. Unfortunately, such mechanisms can be problematic. Even functional programming, which is not inherently sequential (though it is deterministic), precludes concurrency almost by definition.

The last few decades have taught us that introducing concurrency into sequential systems is fiendishly tricky to get right. Rather than carrying on the fool’s errand of building concurrency on top of sequentiality, let us consider building sequentiality on top of concurrency.

Control

The actor model is inherently concurrent. Millions, even billions of actors in a system might be beavering away simultaneously. So much concurrency can be intimidating at first, but worry not. An actor system is perfectly capable of doing one thing at a time. To illustrate this point, we will simulate a JavaScript function using only actors and messages.


function identity(n) {
    return n;
}

It doesn’t get much simpler than identity. But think carefully about what happens when identity is called.


const value = identity(42);

It is passed the argument 42, referred to internally as n, and returns it unchanged. While this happens the caller is frozen, waiting to deposit the return value into the value variable.

How shall we accomplish a similar thing using actors? Firstly, know that actors only act upon receiving a message. They are not allowed to do anything spontaneously. Supposing we have created an echo actor, it will do nothing until we send it a message. This is how we send a message in Humus, an expository actor language:


SEND 42 TO echo

Without even considering the implementation of echo, there is an obvious problem. How is echo supposed to communicate back its return value, 42? Actors do not return. Actors communicate only by sending messages. Unlike functions, they do not have the luxury of blocking the caller whilst they work, meaning that there is no waiting slot in which to deposit a return value.

The solution is to create another actor and send its address along with the argument. Once the echo actor is done, it can send its return value to this customer actor. The customer actor is responsible for whatever comes next, similar to how callback functions are used in JavaScript.


SEND (customer, 42) TO echo

We might implement the echo actor like so:


CREATE echo WITH λ(customer, n).[
    SEND n TO customer
]

The communication protocol for function-like actors might be stated as:

Send me a message containing a customer and arguments and I will send to the customer, exactly once, a message containing the return value.

Of course, it is possible to imagine an actor sending its customer more than one message or even accepting multiple customers. In this regard, actors are far more flexible than functions. Implementing only functions on top of actors would be to miss the point. The point is that actors can, but do not have to, cooperate in a sequential manner via messaging, even though actors themselves operate concurrently. Actors have the power to simulate functions, but the reverse is not true. Messaging is strictly more powerful than function invocation. Sequentiality is built on top of concurrency.

Spacetime

Operations occurring within a processor are kept in lock step by the processor’s clock, which emits an electrical pulse billions of times per second. The clock pulse must reach every component of the processor and give them time to settle before the next pulse is emitted, otherwise they will not be synchronized with each other and chaos will ensue.

Electrons move pretty fast through metal, but neither they nor anything else can exceed the speed of light. It takes time for a clock pulse to travel throughout a processor, and therefore shrinking a processor can unlock faster clock speeds because the clock pulse has a shorter distance to travel. At a certain point, however, further shrinkage becomes too expensive or even physically impossible. For decades it has been cheaper to build computers with multiple processors, each with their own clock, than to make processors faster.

CPU clock speed vs. core count graph

Looking at this graph, it should alarm you that we still think of computation as inherently sequential and communication as inherently synchronous. Clearly this trend towards multiprocessing, as well as the ubiquity of distributed applications, points to an overwhelmingly concurrent future.

The actor model helps us here by admitting the physical reality of time and space. It is expected to take time, possibly a long time, for messages to travel between actors. Messages sent by an actor might not even arrive in the same order that they were sent. We hold similarly loose expectations of IP datagrams moving across the internet, yet upon these little unreliable packets rest big reliable services such as Wikipedia and YouTube.

Special relativity tells us that observers separated by space may disagree on the order in which events occur, except for when those events are related by cause and effect. It follows that there can be no such thing as a universal clock that tells absolute time, and indeed the actor model does not assume such a thing. Individual actors experience the passage of time solely through the cause and effect of sending and receiving messages, relying instead on logical time to coordinate their actions.

Lastly, at the risk of stating the obvious, the universe is concurrent. Everything is happening all the time. Having spent our entire lives experiencing the world, we are completely comfortable with this notion. It is the sequential nature of our programming languages that is unnatural.

State

Likewise, the universe is stateful. Things are changing all the time. But just as there is no universal clock, there is no universal state. It is not even possible for two observers to agree on the current moment, let alone the current state of the universe. We are forced to concede that state is local.

This truth is reflected in the actor model, where each actor manages its own private state. By private, I mean unshared. No actor can access the state of another actor, or affect it in any way, except by sending messages. All messages are immutable, so there is no risk that a communication channel will be forged via mutable values, as is a widespread practice in object-oriented programming. This is a very good thing because communicating via shared mutability can become perilous and slow once concurrency is introduced, due to synchronization mechanisms like locks that can unexpectedly deadlock and halt the entire process. Shared mutable memory also substantially complicates the design of multicore processors.

In response to a message, an actor may choose to update its state. A counter actor, for example, could count how many messages it has received:


DEF counter(n) AS λ(customer).[
    BECOME counter(add(n, 1))
    SEND n TO customer
]
All functions in Humus take a single argument and return a single value. They are written λx.y (or \x.y in ASCII), where x is the argument and y is the return value. Lists are written like a, b, c, so what appears to be multiple arguments in λ(a, b).a is actually a single list value.

But actor state, unlike object state, consists of more than just data. Actors are also capable of changing their code. Because it is impossible to cleanly separate an actor’s code from its data (consider that data can be embedded in code, and code can branch on data) we simply conflate them and say that together they constitute the actor’s “behavior”.

Turnstile as a state machine

The state machine diagram above depicts the operation of a subway turnstile. The turnstile may be in one of two states, locked or unlocked. Feeding a coin into a locked turnstile causes it to transition into an unlocked state, allowing passage, but feeding a coin into an unlocked turnstile leaves its state unchanged.

An actor that transitions through a known set of behaviors is a state machine. Thinking in terms of state machines helps us to shrink the range of possible states in our program, pruning away the many invalid states that can thwart our understanding and cause bugs. When an actor changes its data, code, or both, we say that it “becomes” the new behavior.

Security

The recent rise of supply chain attacks remind us that trusting thousands of open source contributors to consistently act in our best interests is foolish, and indeed negligent when it places our users at risk. It is difficult to resist the fruits of the open source bazaar, however, so let us instead focus on mitigating the risks of using untrusted code. This can be accomplished by applying the Principle Of Least Authority, which states that each software component (be it a function, module, package, or program) should receive the minimum authority required to do its job.

Take for example a function that calculates the distance between two points on the Earth’s surface, published on the internet by a Good Samaritan who has worked through all of the edge cases. This function does not need to read or write files on disk, or access the network, yet most programming languages offer up these powerful capabilities as global variables, giving programmers no way to cordon them off from untrusted code.

Our worst case scenario should be that the function fails to do its job, but instead the worst case scenario is that the function compromises the whole application, or worse, takes control of the user’s machine and holds it to ransom. This is insane. We need ways to safely use untrusted code.

The actor model has several compelling security properties, the most significant having to do with addresses. Actor addresses are unforgeable and opaque, just like object references in a memory-managed language like JavaScript. An actor can only be sent a message if its address is known to the sending actor. The only ways an actor can learn an address is to either be provided the address at creation time, create a new actor with the address, or be sent the address in a message. An actor can not reach out into the global namespace and pluck things out by name, as is permitted in most object-oriented languages.

When powerful capabilities are represented as actors, they are protected from unauthorized use by default. This single fact makes untrusted code much safer to use by denying it all ambient authority. We can even make “facet” actors that sit in front of other actors, forwarding only some kinds of messages. This lets us give out the address of the facet to untrusted actors without exposing the powerful capability itself. Consider a facet actor that sits in front of the filesystem actor, permitting files to be read but never written:


DEF readonly(fs) AS λ(request).[
    CASE request OF
    (#read, _) : [
        SEND request TO fs
    ]
    END
]

This scheme of leveraging unforgeable addresses to provide security is called “object capabilities”, or “ocaps” for short, and was originally devised by Mark S. Miller with object-oriented languages in mind. But it works even better with actors.

Conclusion

Actors were inspired by objects, but they are definitely not objects. Actors are innately concurrent yet this does not impede their ability to coordinate sequentially. The actor model is strictly more general than other models, being able to express everything they can express and more. This is because the actor model respects the true nature of spacetime, rather than sweeping the tricky problem of synchronization under the rug.

The actor model lets us manage state effectively without exposing us to the dangers of shared memory, whilst the natural correspondence of actors to ocaps helps us make complex systems with a much smaller attack surface.


Further reading

Reading about the actor model is most worthwhile. However, the actor model is sufficiently exotic that in order to develop a good feel for it you must read and write programs in an actor language. Though many languages are commonly understood to be actor languages, not all embody the actor model as I’ve described it here so you must be careful. Erlang, for example, misses out on the security benefits of actor addresses because its pids are enumerable.

For hands-on learning and experimentation, I recommend Dale Schumacher’s Humus language. Don’t be put off by the odd syntax, you get used to it. Here is a Fibonacci behavior written in Humus, and here is the quick reference for the language. Many more Humus examples can be found on this blog, where Dale has been publishing original actor research since 2010.

Foundational work on the actor model includes Carl Hewitt’s papers from the 1970s such as this one, as well as Gul Agha’s slim but comprehensive dissertation.