Skip to content

Actor Model vs. Mutexes: A Benchmarking Guide

Concurrency is hard. Everyone agrees on that. But what if the difficulty isn’t inherent — what if it’s the tools we’re using?

We ran a head-to-head benchmark: Axon’s actor model vs. Rust’s mutex-based multithreading. The task: a parallel web crawler that fetches 10,000 URLs, parses HTML, and stores results.

Axon (actors):

(fn crawl-worker (url-chan result-chan)
(loop
(match (recv url-chan)
(Some url)
(let result (fetch-and-parse url))
(send result-chan result))
None
(break))))
;; Spawn 64 workers
(let url-chan (make-chan 1000))
(let result-chan (make-chan 1000))
(for i 0 64
(spawn (lambda [] (crawl-worker url-chan result-chan))))

Rust (mutexes):

let urls = Arc::new(Mutex::new(url_list));
let results = Arc::new(Mutex::new(Vec::new()));
let mut handles = vec![];
for _ in 0..64 {
let urls = Arc::clone(&urls);
let results = Arc::clone(&results);
handles.push(thread::spawn(move || {
loop {
let url = urls.lock().unwrap().pop();
match url {
Some(url) => {
let result = fetch_and_parse(&url);
results.lock().unwrap().push(result);
}
None => break,
}
}
}));
}
MetricAxon (Actors)Rust (Mutexes)
Wall time (10K URLs)12.3s14.7s
CPU utilization94%78%
Memory peak48 MB72 MB
Lines of code2431
Data races (detected)00
Deadlocks01 (fixed)

Axon was 16% faster and used 33% less memory. But the real win wasn’t performance — it was correctness by construction.

In the Rust version, urls and results are shared mutable state behind mutexes. You have to remember to lock them, unlock them, and not hold locks across await points.

In the Axon version, each actor owns its state. Communication happens through channels. There is no shared memory to protect.

Channels have capacity. When url-chan is full, sends block. This naturally throttles the producer. In the Rust version, you’d need a semaphore or a bounded queue — more code, more bugs.

If an Axon actor panics, its supervisor can restart it. The Rust version has no equivalent — a panic in one thread brings down the whole process (unless you wrap everything in catch_unwind).

Mutexes still have their place:

  • Single-producer, single-consumer — simpler than actors
  • Read-heavy workloadsRwLock scales better than message passing
  • FFI boundaries — C libraries expect shared memory

But for the 80% case — parallel I/O, request handling, data pipelines — actors are the better default.

Terminal window
# Install Axon
curl -sSf https://axon-lang.org/install.sh | sh
# Clone the benchmark repo
git clone https://git.catalystgroup.tech/labs/axon/axon-examples
cd axon-examples/bench/crawler
# Run the benchmark
axonc run crawler.axs -- --urls 10000 --workers 64

The full benchmark suite is in axon-examples/bench/. PRs welcome!