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.
The Setup
Section titled “The Setup”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, } } }));}The Results
Section titled “The Results”| Metric | Axon (Actors) | Rust (Mutexes) |
|---|---|---|
| Wall time (10K URLs) | 12.3s | 14.7s |
| CPU utilization | 94% | 78% |
| Memory peak | 48 MB | 72 MB |
| Lines of code | 24 | 31 |
| Data races (detected) | 0 | 0 |
| Deadlocks | 0 | 1 (fixed) |
Axon was 16% faster and used 33% less memory. But the real win wasn’t performance — it was correctness by construction.
Why Actors Win
Section titled “Why Actors Win”1. No Shared State
Section titled “1. No Shared State”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.
2. Backpressure Is Free
Section titled “2. Backpressure Is Free”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.
3. Supervision Is Built In
Section titled “3. Supervision Is Built In”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).
When to Use Mutexes
Section titled “When to Use Mutexes”Mutexes still have their place:
- Single-producer, single-consumer — simpler than actors
- Read-heavy workloads —
RwLockscales 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.
Try It Yourself
Section titled “Try It Yourself”# Install Axoncurl -sSf https://axon-lang.org/install.sh | sh
# Clone the benchmark repogit clone https://git.catalystgroup.tech/labs/axon/axon-examplescd axon-examples/bench/crawler
# Run the benchmarkaxonc run crawler.axs -- --urls 10000 --workers 64The full benchmark suite is in axon-examples/bench/. PRs welcome!