If you have written concurrent code with threads, you know the routine. Several threads touch the same data, so you put a lock around it. Then you add a second lock, take the two in the wrong order on a busy day, and two threads wait for each other forever. Or you forget the lock once, and a counter is off by one in production every few weeks.
The actor model starts from a different question: what if no two pieces of code ever shared memory at all?
This article covers where the idea came from, the problem each step in its history answered, and how an actor works, in pictures. A follow-up article looks at three companies that run actors in production.
The problem: shared memory and locks
Here is a tiny shop with one item left in stock, written two ways.

Left: every thread must remember the lock. Right: only the shop touches stock, so there is nothing to lock.
On the left, three threads read and write one stock variable. Correctness depends on every thread taking the lock, every time, in the same order. The lock also makes threads that want the stock run one after another, which is the opposite of why you added threads. Akka's documentation sums up the second risk in five words: "Locks introduce a new menace: deadlocks."
On the right, the stock belongs to one actor, the shop. Nobody else can read or write it. Anyone who wants to buy sends the shop a message, and the shop handles its messages one at a time. There's no shared variable, so there is nothing to lock.
That is the whole idea. The rest of this article explains how the idea grew up, and what you need around it to build real systems.
Where actors came from

Navy: the theory. Violet: Erlang and OTP, where supervision comes from. Blue and amber: the model reaching the JVM and .NET.
1973: a theory with one kind of object
In August 1973, Carl Hewitt, Peter Bishop and Richard Steiger presented A Universal Modular ACTOR Formalism for Artificial Intelligence at the third International Joint Conference on Artificial Intelligence (IJCAI). Their proposal was radical in its simplicity: everything is an actor, and everything an actor does is send messages to other actors.
The problem they were answering was how to describe computation that happens in many places at once. The usual tools of the time were goto, interrupts and shared variables guarded by semaphores. The paper says its formalism "does not have or need goto, interrupt, or semaphore primitives". Message passing replaces all of them.
1985: what an actor does with a message
The 1973 paper was broad. Gul Agha's 1985 MIT technical report, ACTORS: A Model of Concurrent Computation in Distributed Systems (a book from MIT Press in 1986), pinned the model down. When an actor receives a message, it responds with three things:
- a set of messages it sends to other actors,
- a set of new actors it creates,
- a replacement behavior, which decides how it handles the next message.
Every actor has a mail address, and an actor can only send to addresses it knows. That is the definition most actor systems still follow. Hewitt's later papers state the same three actions and add a point worth remembering: the model itself does not require a mailbox or a thread. Those are implementation choices, though nearly every practical system adds a mailbox.
1986: Erlang reaches the same design by another road
At the Ericsson Computer Science Laboratory, Joe Armstrong, Robert Virding and Mike Williams started work on a new language in 1986. Armstrong's history of the language gives its goal: "a better way of programming telephony applications". A telephone switch handles a huge number of calls at once, must keep running for years, and can't stop because one call hit a bug.
Erlang's answer was many small processes that share no memory and communicate only by asynchronous messages. That is an actor in everything but name, and the name was missing on purpose. On a 2019 panel with Hewitt and Tony Hoare, Armstrong said: "I didn't know about the actor stuff at the time". In a 2014 mailing-list post he named Prolog and Smalltalk as Erlang's influences instead. Two groups, a theorist and a telecom lab, arrived at the same design from different problems.
1996 to 2003: supervision and "let it crash"
Ericsson started the Open Telecom Platform (OTP) project in 1996: a set of Erlang libraries for building telecom systems. It released Erlang and OTP as open source in December 1998.
Armstrong's 2003 PhD thesis, Making reliable distributed systems in the presence of software errors, explains the error-handling style behind OTP. Its slogan is "Let it crash." Instead of writing defensive code for every possible failure, a process that hits an unexpected error simply dies, and a separate process, its supervisor, recovers. Supervisors watch workers and other supervisors, so a whole system becomes a tree of them. We'll come back to this below.
The thesis also reports on Ericsson's AXD301 ATM switch, which had over 1.7 million lines of Erlang in 2003. You may have seen the famous "nine nines" (99.9999999%) availability figure for it. The thesis does quote a customer's figure of that size, and it also notes that how the figure was obtained was not documented. Treat it as a story about a very reliable system, not a measurement.
2009: actors on the JVM
Jonas Bonér started Akka in 2009 (its first commit is from February that year). Akka brought actors, mailboxes and supervision to the Java Virtual Machine, with Scala and Java APIs, so JVM teams could use the model without leaving their platform. Akka's documentation still opens with the problem it answers: the costs of locks and shared memory on modern CPUs.
2014: virtual actors
In 2014, Microsoft Research described Orleans, a .NET framework built around virtual actors. In classic actor systems you create an actor, keep its address, and stop it when you are done. Orleans actors are, in the paper's words, "purely logical entities that always exist, virtually". You never create or destroy them: you call an actor by its ID, and the runtime activates it on some server if it isn't already running. The problem it answered was cloud services with millions of objects, such as players, where managing each actor's lifecycle by hand is a burden. The second article shows Orleans running a real game backend.
How an actor works
With the history in place, here is the model in one picture.

An actor is an address, a mailbox and a behavior with private state. For each message it can do only the three things on the right.
An actor has three parts:
- An address. You can send a message only to an actor whose address you have. You get one by creating the actor, or by receiving the address in a message. The address is how the rest of the system refers to the actor; nobody holds a pointer to its insides.
- A mailbox. Messages wait here until the actor takes them. Sending is asynchronous: the sender drops the message off and carries on.
- A behavior with private state. The actor takes one message at a time, runs its behavior to completion, and only then takes the next. Only the actor reads or writes its state.
For each message, the behavior can do three things: send messages to addresses it knows, spawn new actors, and become a new behavior for the next message. Agha called the last one a replacement behavior; Akka's classic API calls it become. In practice, "become" is often just "update my own state", like setting total from 6 to 7.
Here is a counter actor in pseudocode:
actor Counter(total = 0):
receive Add(n):
total = total + n # only this actor ever touches total
receive Get(replyTo):
send(replyTo, Total(total)) # answer with a message, not a return value
Notice what's missing: no lock, no synchronized, no atomic. total can't be corrupted by two threads, because only one message is ever being handled, and only this actor can see total. Akka's docs make the same point: because at most one message is processed per actor at a time, the actor's invariants hold without synchronization.
Also notice Get. There is no return value, because there is no call stack shared between the two actors. The asker includes its own address, and the counter replies with a message.
One message at a time
The mailbox is what turns concurrent senders into sequential processing. Here, three people use one bank account at the same moment:

Watch the balance box: it changes only when the account takes the next message from its mailbox, never while another message is in progress.
alice, bob and carol run concurrently, but the account handles their messages strictly one after another. It needs no lock on balance.
The last frame of the animation is the honest part. The order in which messages from different senders arrive is not fixed. In this run carol is declined; in another, bob would be. Actors remove data races, but they don't remove race conditions in your business logic. If order matters, you have to design for it, for example by putting the decision in one actor that sees all the requests, or by making the protocol explicit.
Location transparency
Because an actor is known only by its address, the sender's code doesn't depend on where the receiver runs.

Path 1 stays in memory. Path 2 is serialized and crosses the network. The send call looks the same for both.
Akka calls this location transparency. Its documentation says its functions work the same in a single JVM or on a cluster, because every interaction between actors is an asynchronous message anyway.
This is what lets an actor system grow from one machine to many without rewriting the code that sends messages. The warning icon in the picture is the catch. The code is the same, but the physics isn't: a remote message is slower and can be lost. Well-designed actor systems treat every message as one that might not arrive, and use timeouts and retries where it matters.
Let it crash: supervision trees
Actors that create other actors form a tree. In Erlang/OTP and Akka, each parent supervises its children: when a child fails, the parent decides what happens.

Follow worker-2's count: 41 before the crash, 0 after the restart. Its siblings never stop counting.
The numbered steps in the animation are the whole recovery:
- A bad message makes
worker-2crash. It doesn't try to recover in place. - Its supervisor,
payments, is told that the child exited. - The supervisor's strategy picks what to do. Here it's OTP's
one_for_one: restart only the failed child. - The worker's old, possibly corrupt, state is thrown away.
- It starts again from a known good state.
- New messages reach the restarted worker, and the siblings never noticed.
OTP's supervisor documentation defines other strategies too. one_for_all restarts every child when one fails, for children that only make sense together. And restarts are bounded: if a child needs more than a set number of restarts within a set number of seconds (by default, more than 1 restart in 5 seconds), the supervisor gives up, stops its children and itself, and its own parent decides what to do next. A local problem that a restart can't fix gets escalated up the tree.
Why is crashing a good idea? Because most production bugs are rare combinations of state and input. Code that tries to handle every unexpected case gets long and usually wrong. A fresh start from known state fixes the common cases, keeps the error-handling code in one place (the supervisor), and keeps the failure contained to one small actor instead of the whole process.
What actors buy you, compared with locks
| Shared memory and locks | Actors | |
|---|---|---|
| Who touches the data | Any thread with a reference | Only the owning actor |
| Data races | Possible whenever a lock is missed | Impossible on actor state |
| Deadlocks from lock ordering | Possible | No locks to order |
| Failure | An exception unwinds one thread's stack, often far from anyone who can fix it | The actor dies and its supervisor decides |
| Scaling out | A rewrite: shared memory doesn't cross machines | The same send, to an address on another node |
Actors are not free, and it's worth being clear about the costs:
- Ordering still matters. As the account example showed, messages from different senders can arrive in any order.
- Mailboxes can fill up. If an actor receives messages faster than it handles them, its mailbox grows. Production systems need back-pressure, bounded mailboxes, or both. The second article shows a real case of exactly this.
- Two actors can still wait on each other. If A waits for a reply from B while B waits for a reply from A, neither progresses. Timeouts help; so does designing protocols that don't need round trips.
- Debugging is different. A bug is a sequence of messages, not a stack trace, so good tracing and logging matter more.
When to reach for actors
Actors fit best when your problem already looks like many independent things with their own state that talk to each other: users, sessions, devices, game players, chat rooms, orders. Each of those becomes an actor, and the runtime spreads them over your cores and machines.
They fit less well for bulk number crunching over shared arrays, or for simple request-response services that keep no state between requests.
The model is fifty years old, and it has outlived many of the languages that first used it, because the problem it answers hasn't changed. Concurrent programs are hard when everything can touch everything. Actors make each piece of state belong to exactly one owner, and they turn failure into something a supervisor can deal with.
In the next article, we'll look at how Discord, LinkedIn and the team behind Halo used actors to serve millions of users.
Sources
- Hewitt, Bishop, Steiger. A Universal Modular ACTOR Formalism for Artificial Intelligence. IJCAI 1973, pp. 235–244. https://www.ijcai.org/Proceedings/73/Papers/027B.pdf
- Agha. ACTORS: A Model of Concurrent Computation in Distributed Systems. MIT AI Lab Technical Report 844, June 1985. https://dspace.mit.edu/handle/1721.1/6952
- Hewitt. Actor Model of Computation: Scalable Robust Information Systems. arXiv:1008.1459. https://arxiv.org/abs/1008.1459
- Armstrong. A History of Erlang. HOPL III, 2007. https://doi.org/10.1145/1238844.1238850
- Erlang FAQ, "Academic and Historical Questions". https://www.erlang.org/faq/academic.html
- Erlang Solutions. Let's #TalkConcurrency panel discussion with Sir Tony Hoare, Joe Armstrong and Carl Hewitt, 2019. https://www.erlang-solutions.com/blog/lets-talkconcurrency-panel-discussion-with-sir-tony-hoare-joe-armstrong-and-carl-hewitt/
- Armstrong. "Erlang is not a implementation of the Actor model", erlang-questions mailing list, 25 June 2014. http://erlang.org/pipermail/erlang-questions/2014-June/079891.html
- Armstrong. Making reliable distributed systems in the presence of software errors. PhD thesis, KTH, December 2003. https://erlang.org/download/armstrong_thesis_2003.pdf
- Erlang/OTP documentation, "Supervisor Behaviour". https://www.erlang.org/doc/system/sup_princ.html
- Akka documentation, "Why modern systems need a new programming model". https://doc.akka.io/libraries/akka-core/current/typed/guide/actors-motivation.html
- Akka documentation, "How the Actor Model Meets the Needs of Modern, Distributed Systems". https://doc.akka.io/libraries/akka-core/current/typed/guide/actors-intro.html
- Akka documentation, "Location Transparency". https://doc.akka.io/libraries/akka-core/current/general/remoting.html
- Akka documentation, "Classic Actors" (Become/Unbecome). https://doc.akka.io/libraries/akka-core/current/actors.html
- Akka's first commit, 16 February 2009. https://github.com/akka/akka-core/commit/0a31ad718883ef0314de6dc8c6c2c9699eadfdae
- Bernstein, Bykov, Geller, Kliot, Thelin. Orleans: Distributed Virtual Actors for Programmability and Scalability. Microsoft Research MSR-TR-2014-41, March 2014. https://www.microsoft.com/en-us/research/publication/orleans-distributed-virtual-actors-for-programmability-and-scalability/
Top comments (0)