Scale and concurrency benchmark
cmd/bench is a load harness that drives many durable agent runs concurrently in one process
against a stub model, so it measures what the framework adds (orchestration, journaling, the
agent loop, concurrency) rather than LLM latency. Optional -latency simulates per model-call
I/O wait to show how Go absorbs large concurrent I/O-bound fan-out.
go run ./cmd/bench -runs 20000 -concurrency 5000 -latency 50ms
What it shows
Agent workloads are overwhelmingly I/O-bound: a run spends its wall-clock waiting on model and
tool calls. Go's scheduler and cheap goroutines mean one process can keep a very large number of
those durable, journaled runs in flight at once. That is the "library, not cluster" pillar with
the concurrency turned up.
Measured
Two environments, medians reported. Every run journals to the in-memory MemStore, so the
journal-records figure is an in-memory write rate, not a durable-store one. Each run writes five
journal records in these scenarios.
A standard GitHub Actions runner (ubuntu-latest, 4 vCPU AMD EPYC 7763, Go 1.27), each
scenario run 21 times, from the Benchmark workflow at v0.8.0.
Anyone can reproduce these: Actions, Benchmark, Run workflow. Runner CPU models vary between
jobs, so compare runs on the same model.
| Scenario |
Wall-clock |
Runs/s |
Journal records/s |
p50 |
p99 |
Peak goroutines |
Heap delta |
Overhead: -runs 5000 -concurrency 256 |
~211 ms |
~23,700 |
~118,400 |
0.98 ms |
52 ms |
304 |
~14 MB |
I/O fan-out: -runs 20000 -concurrency 5000 -latency 50ms |
~1.05 s |
~19,000 |
~95,000 |
222 ms |
419 ms |
5,805 |
~64 MB |
A 10-core Apple silicon Mac (darwin/arm64, Go 1.27), measured at v0.7.0, each scenario run
seven times, when each run wrote four journal records. It was measured with other work running (a
virtual machine using a full core, load average about 4), so treat it as a lower bound for that
machine.
| Scenario |
Wall-clock |
Runs/s |
Journal records/s |
p50 |
p99 |
Peak goroutines |
Heap delta |
Overhead: -runs 5000 -concurrency 256 |
~95 ms |
~52,700 |
~210,700 |
2.4 ms |
25 ms |
319 |
~9 MB |
I/O fan-out: -runs 20000 -concurrency 5000 -latency 50ms |
~469 ms |
~42,600 |
~170,500 |
101 ms |
139 ms |
5,563 |
~62 MB |
The fan-out rows are the point. Each run makes two model calls at 50ms each, so ~100ms of
unavoidable wait, and 20,000 of them, 5,000 in flight at a time, finish in about half a second on
the Mac (measured at v0.7.0), where the median run takes the model's own ~100ms because the runs
fully overlap. On the 4-vCPU runner the same work takes about one second: with fewer cores the
framework's CPU work, not the model wait, sets the pace. Either way it is a few thousand goroutines
and tens of MB in one commodity process instead of a cluster.
What this does NOT claim
- Not lower latency than the model. The provider dominates per-call wall-clock; the win is
throughput, concurrency, and operational simplicity, not per-call speed.
- The stub model has no real inference; this measures framework overhead and concurrency, not a
provider.
MemStore is the in-memory floor. In production the durable store's write throughput
(SQLite, Postgres, partitioned) is the real ceiling at high fan-out, not goroutines. Benchmark
against your store to find that limit; do not quote these numbers as a production SLA.