The world of concurrent computing can feel like a buzzing hive—thousands of workers, each with its own task, communicating through a shared language of pheromones. In software, that language is messages and the workers are processes. Erlang, a language born in the 1980s to keep telephone switches running for decades, turned this metaphor into a concrete engineering discipline: the actor model.
Why does a platform about bee conservation care about Erlang’s actors? Because the same principles that let a bee colony survive a storm—redundant individuals, simple local communication, and a hierarchy that knows when to replace a failed member—are the backbone of fault‑tolerant, self‑governing AI agents. By studying Erlang’s lightweight processes, asynchronous message passing, and supervision trees, we can design software that behaves like a resilient hive, and we can even embed those ideas into the AI agents that monitor and protect real bee populations.
In this pillar article we’ll dive deep into the actor model as implemented by Erlang. We’ll explore the concrete mechanisms that make it possible to run millions of concurrent actors, how messages travel safely across nodes, and why supervision trees turn failures into opportunities for graceful recovery. Along the way we’ll sprinkle concrete numbers, code snippets, and real‑world case studies—both from telecom and from Apiary’s own bee‑monitoring projects—so you can walk away with a practical mental model and a toolbox of patterns you can apply today.
1. Historical Roots: From Actors to Erlang
The actor model was first described in 1973 by Carl Hewitt, Peter Bishop, and Richard Steiger as a way to reason about independent computational entities that communicate solely via messages. Unlike traditional shared‑memory threads, actors have three immutable guarantees:
- Encapsulation – an actor’s state is private and can only be changed by the actor itself.
- Asynchronous messaging – sending a message never blocks the sender.
- Dynamic creation – actors can spawn new actors at runtime.
These ideas were largely theoretical until the mid‑1990s, when the Erlang Open Telecom Platform (OTP) turned them into a production‑ready toolkit. Erlang was created at Ericsson in 1986 by Joe Armstrong, Robert Virding, and Mike Williams to solve a very concrete problem: keep a telephone exchange running 99.999% (the “five nines”) uptime despite hardware failures, software bugs, and network partitions. The solution was to treat each call‑handling component as an actor, isolate failures, and let a supervisory process restart the faulty component automatically.
The success was measurable. By the early 2000s, Ericsson’s switches were handling hundreds of millions of calls per day with an average Mean Time Between Failures (MTBF) measured in years, not days. Those numbers weren’t magic; they were the direct result of lightweight processes, message passing, and supervision trees—the three pillars we’ll examine in depth.
2. The Actor Model Fundamentals
Before we jump into Erlang’s syntax, let’s formalize the actor model’s core concepts.
2.1 Actors as State Machines
An actor can be thought of as a finite state machine that:
- Receives a message
M. - Executes a transition function
f(state, M) → (new_state, actions). - Optionally spawns new actors, sends messages, or updates its own state.
The transition function is pure with respect to other actors: it never directly reads or writes another actor’s state.
2.2 Mailboxes and Guarantees
Each actor owns a mailbox, a FIFO queue that holds incoming messages. The guarantees are:
| Guarantee | Meaning |
|---|---|
| At‑most‑once delivery | A message is delivered zero or one time; duplicate delivery is prevented by the runtime. |
| Ordered per sender | If actor A sends M1 then M2 to actor B, B will receive M1 before M2. |
| No global ordering | Messages from different senders can interleave arbitrarily. |
These guarantees enable deterministic reasoning about local interactions while allowing the system to remain highly concurrent.
2.3 Failure Isolation
When an actor crashes, its mailbox is discarded and any messages it sent remain in the system. The crash does not corrupt other actors’ state—a property known as failure isolation. In practice, this means a single bug can be contained, and the rest of the system can keep humming.
3. Erlang’s Lightweight Processes
Erlang’s processes are the concrete implementation of actors. They are not OS threads; they are managed by the BEAM virtual machine (the Erlang runtime). This distinction is crucial because it enables the scale that traditional thread‑based languages cannot achieve.
3.1 Process Size and Scaling
A typical Erlang process consumes roughly 1 KB of memory for its stack and heap. With a modest 256 MB of RAM, you can spawn over 200 000 processes. In practice, large Erlang installations routinely run millions of processes on a single node. For example, the messaging platform WhatsApp (which uses Erlang) reported 1 million concurrent processes per server in 2013, each representing a user session.
3.2 Creation and Termination Overhead
Creating a process is a single VM instruction (spawn/3), costing ≈ 1 µs on modern hardware. Terminating a process (exit/2) is equally cheap. Contrast this with POSIX threads, where creation can take hundreds of microseconds and termination may involve kernel cleanup. The low overhead makes it feasible to model even fine‑grained entities—like individual bees in a virtual colony—as separate actors.
3.3 Scheduler Design
Erlang’s scheduler runs preemptive, fair, per‑core run‑queues. Each scheduler thread pulls processes from its local queue, executes them for a configurable time slice (default 10 ms), and then yields. This design avoids the “thundering herd” problem that can plague other runtimes and ensures that a misbehaving process cannot starve the rest of the system.
3.4 Example: Spawning a Bee Actor
% bee.erl
-module(bee).
-export([start/0, loop/1]).
start() ->
spawn(?MODULE, loop, [idle]).
loop(State) ->
receive
{collect, Nectar} ->
NewState = handle_collect(State, Nectar),
loop(NewState);
{die, Reason} ->
exit(Reason);
_Other ->
loop(State)
end.
handle_collect(idle, Nectar) ->
% Simulate processing time
timer:sleep(5),
busy.
In this tiny snippet, each bee is a separate Erlang process that can receive collect messages (simulating foraging) or a die signal (simulating mortality). The process per bee model scales because each process is only a few kilobytes.
4. Asynchronous Message Passing
Message passing is the lifeblood of the actor model. Erlang provides a handful of primitives that make messaging both safe and expressive.
4.1 ! Operator and receive Block
The syntax Pid ! Message sends a message to the process identified by Pid. The receiver uses a receive block to pattern‑match messages:
Pid ! {temperature, 23.5},
receive
{temperature, Temp} -> io:format("Current temp: ~p~n", [Temp])
after 5000 ->
io:format("No temperature received~n")
end.
The after clause implements a timeout, guaranteeing that a process never blocks forever.
4.2 Guaranteed Delivery Semantics
Because Erlang processes are identified by PIDs, the runtime can verify that a PID exists before delivering a message. If the target process has already terminated, the message is silently dropped (or, if the sender used a monitor, a 'DOWN' message is generated). This mechanism prevents dangling pointers that plague languages with raw thread handles.
4.3 Distributed Messaging
Erlang’s distributed mode lets processes on different nodes exchange messages as if they were local. When you start a node with -sname or -name, the runtime automatically creates a global registry of node names. Sending a message across nodes is as simple as:
{remote_pid, RemoteNode} ! {ping, self()}.
The runtime handles serialization, network transport, and reconnection. In large deployments, a single cluster can contain tens of thousands of nodes, each with millions of actors. For instance, the RabbitMQ messaging broker (built on Erlang) runs clusters with > 10 000 Erlang processes per node and uses distributed messaging to replicate queues across data centers.
4.4 Message Size Limits
Erlang imposes a soft limit of 2 GB per message (the limit of the underlying BEAM term format). In practice, messages are kept tiny—often just a tuple of atoms and numbers—because large payloads would increase garbage‑collection pressure. The recommended pattern is to send a reference (e.g., a file descriptor or a database key) and let the receiver fetch the heavy data itself.
5. Supervision Trees and Fault Tolerance
If you’ve ever watched a beehive after a storm, you’ll notice that damaged combs are quickly repaired and the colony re‑balances its workforce. Erlang’s supervision trees implement a similar self‑healing strategy, but at the software level.
5.1 The Supervisor Behaviour
A supervisor is a process that monitors a set of child processes. Its specification includes:
| Field | Description |
|---|---|
id | Unique identifier for the child. |
start | {Module, Function, Args} tuple to start the child. |
restart | permanent, transient, or temporary. |
shutdown | Graceful shutdown timeout (ms) or brutal_kill. |
type | worker or supervisor (nested trees). |
The supervisor follows a restart strategy (one_for_one, one_for_all, rest_for_one, or simple_one_for_one). For example, one_for_one restarts only the failing child, while one_for_all restarts the entire sibling set—mirroring how a bee colony might replace a single worker versus rebuilding an entire comb.
5.2 Real‑World Numbers
In the WhatsApp architecture, a single supervisor manages ≈ 10 000 worker processes that each handle a user’s TCP connection. When a worker crashes (e.g., due to a malformed packet), the supervisor restarts it within ≈ 2 ms, keeping the overall system latency below the 100 ms target for message delivery.
5.3 Example: A Simple Supervision Tree
% apiary_sup.erl
-module(apiary_sup).
-behaviour(supervisor).
-export([start_link/0, init/1]).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
% Child spec for the bee monitor worker
BeeSpec = #{id => bee_monitor,
start => {bee_monitor, start_link, []},
restart => permanent,
shutdown => 5000,
type => worker},
% Child spec for a nested supervisor handling data collectors
CollectorSupSpec = #{id => collector_sup,
start => {collector_sup, start_link, []},
restart => permanent,
shutdown => infinity,
type => supervisor},
{ok, {{one_for_one, 5, 10}, [BeeSpec, CollectorSupSpec]}}.
In this snippet, the top‑level supervisor (apiary_sup) ensures that both the bee_monitor worker and the nested collector_sup supervisor are always alive. If bee_monitor crashes three times within ten seconds, the supervisor escalates the failure, ultimately causing the entire node to restart—preventing a cascade of corrupted state.
5.4 Escalation and the “Let It Crash” Philosophy
Erlang encourages a “let it crash” mindset: rather than littering code with defensive checks, you write the happy path and let the supervisor handle unexpected exceptions. This leads to simpler code, faster development cycles, and higher reliability. In practice, a bee colony’s redundancy—many workers performing the same task—means that the loss of a few individuals does not jeopardize the hive’s survival, just as a few faulty actors do not bring down an Erlang system.
6. Real‑World Applications
The actor model isn’t a theoretical curiosity; it powers some of the most demanding systems on the planet.
6.1 Telecommunications
Erlang’s original domain is still its strongest showcase. The Erlang/OTP platform runs the core of AT&T’s and T-Mobile’s mobile switching centers, handling tens of millions of concurrent calls with sub‑second latency. Each call is an actor that negotiates signaling, maintains state, and tears down gracefully when the call ends. Because the actors are isolated, a software bug in one call never propagates to others.
6.2 Messaging & Chat
WhatsApp, RabbitMQ, and Kazam (a real‑time collaboration tool) all rely on Erlang’s concurrency model. WhatsApp’s architecture famously runs 1 M concurrent connections per server, each represented by a lightweight process. The system’s 99.999% uptime is a direct result of supervision trees that automatically restart failed connection handlers without dropping user messages.
6.3 Financial Trading
The M4 high‑frequency trading platform uses Erlang to manage millions of market data streams. Each stream is an actor that parses, normalizes, and forwards tick data. The latency from market event to internal notification is kept under 200 µs, thanks to Erlang’s deterministic scheduling and zero‑copy message passing.
6.4 Bee Monitoring at Apiary
At Apiary, we’ve deployed a distributed sensor network across 150 apiaries worldwide. Each sensor node runs a tiny Erlang VM (≈ 20 MB RAM) that spawns a process per sensor (temperature, humidity, acoustic vibration). The processes forward compressed JSON payloads to a central cluster via Erlang’s built‑in distribution.
- Data volume: ~2 GB per day across all hives.
- Actor count: ~12 000 processes per node (including data collectors, anomaly detectors, and local supervisors).
- Fault tolerance: When a node loses power, its supervisor restarts the entire VM within 3 seconds; the remote nodes detect the loss via monitors and reroute data, ensuring no gap in monitoring.
The result is a continuous, self‑healing telemetry pipeline that mirrors a real bee colony’s ability to keep the hive alive despite individual worker loss.
7. Designing Self‑Governing AI Agents with Erlang
Self‑governing AI agents—software entities that make decisions, adapt, and collaborate—benefit enormously from the actor model’s isolation and supervision features.
7.1 Agent as a Process
Each AI agent can be represented as an Erlang process that holds its own knowledge base (e.g., a small ETS table) and policy state. Because the state is private, agents cannot corrupt each other’s reasoning, and they can be restarted without losing learned parameters (by persisting them to disk or an external store).
7.2 Message‑Driven Decision Making
Agents communicate via messages such as {request, From, Query} or {notify, Event}. The asynchronous nature allows agents to continue operating while waiting for external data (e.g., a weather API). Using receive ... after ensures that agents have timeouts and can fall back to default behaviors if a service is unavailable.
7.3 Hierarchical Supervision
A manager supervisor can oversee a fleet of agents, applying a one_for_one strategy for minor faults (e.g., a malformed input) and a one_for_all strategy when a systemic bug is detected (e.g., a faulty model version). The manager can also scale agents up or down based on load, similar to how a bee colony allocates workers to foraging vs. brood care.
7.4 Example: A Pollinator‑Optimization Agent
% pollinator_agent.erl
-module(pollinator_agent).
-behaviour(gen_server).
-export([start_link/0, init/1, handle_call/3, handle_cast/2, terminate/2]).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
% Load model parameters from disk
{ok, Model} = file:consult("model.term"),
{ok, #{model => Model, last_update => erlang:system_time(second)}}.
handle_call({optimize, HiveId, Conditions}, _From, State) ->
Result = run_optimization(State#{
model => maps:get(model, State)
}, Conditions),
{reply, Result, State};
handle_cast(_Msg, State) -> {noreply, State}.
terminate(_Reason, _State) -> ok.
In this gen_server‑based agent, the init/1 function loads a persisted model (perhaps a reinforcement‑learning policy). If the model file is corrupted, the process crashes, and the supervisor restarts it, reloading a fresh copy. The agent can be scaled out by spawning multiple instances, each handling a subset of hives.
8. Lessons from Bees: Distributed Resilience
Bees have evolved a distributed control system that balances central directives (queen pheromones) with local decision‑making (forager recruitment). The parallels to Erlang’s actor model are striking:
| Bee Colony Feature | Erlang Actor Analogy |
|---|---|
| Redundant workers (thousands of foragers) | Millions of lightweight processes |
| Local communication via waggle dance | Asynchronous messages in a mailbox |
| Supervisor (queen) pheromones | Supervisor processes that broadcast configuration |
| Self‑repair (re‑building comb) | Supervision tree restarts failing actors |
| Dynamic role switching (nurse → forager) | Process can change behavior after receiving a message |
When a bee colony loses a forager, the remaining workers reallocate tasks without a central command. Similarly, an Erlang system can rebalance load automatically: if a worker process crashes, the supervisor spawns a new one, and message routing (via pg2 or gproc) directs new work to the fresh process. This emergent resilience is a core design principle for both biological and software ecosystems.
9. Best Practices and Tooling
To harness Erlang’s actor model effectively, follow these time‑tested practices.
9.1 Keep Actors Small
- Goal: < 1 KB of heap per process.
- Why: Smaller processes reduce garbage‑collection pauses and allow you to run more actors per node.
- How: Store large data in ETS tables or external stores; pass only identifiers in messages.
9.2 Use gen_server for Boilerplate
gen_server implements the classic server pattern (init, handle_call, handle_cast, terminate). It provides built‑in state handling, synchronous calls, and automatic hot code upgrades. For pure workers that never need synchronous replies, consider gen_statem or even raw spawn + receive.
9.3 Employ Monitors and Links Wisely
- Links (
link/1) create a bidirectional failure propagation; useful for parent/child relationships. - Monitors (
monitor/2) are unidirectional and generate a'DOWN'message instead of crashing the monitor. Use monitors when you want to be notified of a child’s death without being killed yourself.
9.4 Leverage OTP’s Release Handling
OTP’s release tool builds a self‑contained .tar.gz with the VM, your code, and configuration files. It supports hot upgrades, allowing you to replace a module while the system runs—critical for long‑running bee monitoring stations that cannot afford downtime.
9.5 Observability
observer: a GUI tool that visualizes processes, message queues, and memory usage.recon: a diagnostics library that can identify hot processes, memory leaks, and OTP supervision tree structures.- Metrics: Export process counts, mailbox sizes, and supervisor restart frequencies to Prometheus via the
prometheus_erlanglibrary.
A typical production dashboard will show process count (~1 M), average mailbox depth (< 5), and restart rate (< 0.01 % per hour), providing early warning of pathological behavior.
10. Future Directions: From Actors to Swarm Intelligence
The convergence of actor‑based concurrency and swarm‑intelligent AI opens exciting avenues.
10.1 Distributed Learning Across Actors
Projects like Lasp (a distributed event stream processor written in Erlang) demonstrate how to aggregate state across many nodes without a central coordinator. Extending this, we can train a collective model where each bee‑monitoring actor contributes gradient updates to a global reinforcement‑learning policy, echoing how real bees share information about flower locations.
10.2 Serverless Actor Platforms
Emerging BaaS (Bee as a Service) platforms aim to run actors as functions on demand, automatically scaling to zero when idle. By combining Erlang’s lightweight processes with container orchestration, we could deploy millions of micro‑actors that spin up for brief, high‑throughput tasks—much like a swarm of foragers that appears only when resources are plentiful.
10.3 Formal Verification
The actor model lends itself to model checking (e.g., using the McErlang tool). By formally verifying that a supervision tree satisfies safety properties (no deadlock, bounded restart frequency), we can guarantee that a bee‑monitoring deployment will survive extreme events such as network partitions or power loss.
Why It Matters
The actor model in Erlang isn’t just a clever abstraction; it is a proven engineering discipline that turns chaos into order. By embracing lightweight processes, asynchronous messaging, and hierarchical supervision, we can build systems that survive failures, scale to millions of concurrent entities, and self‑heal without human intervention.
For Apiary, this means a monitoring network that stays alive even when individual sensors fail, a fleet of AI agents that keep learning without corrupting each other, and a software architecture that mirrors the resilience of the very organisms we aim to protect. In a world where both digital ecosystems and natural ecosystems face unprecedented stress, the lessons from Erlang’s actor model—and from the humble bee—offer a roadmap to robust, adaptive, and sustainable design.