Performance
The project repeats the simple process benchmark from the 2021 article Zeebe and Camunda under high load on the same machine: Intel Core i7-4770K, four cores, 24 GB RAM and SSD storage.
Results at a glance#
- With PostgreSQL history, synchronous methods reach 1 153 instances/s, and service tasks reach 419 instances/s.
- Without history, methods reach 1 361 instances/s. Every process step still commits to PostgreSQL.
- History adds roughly 10–15% overhead in these runs because its rows share the step transaction.
- With
synchronous_commit=off, service tasks with history reach 752 instances/s, with the durability tradeoff explained below.
Comparison charts#
Throughput is completed instances divided by elapsed time from the first start, for 100,000 starts. go-bpm bars are the median of three runs; hover or focus a bar to see the range. Other systems' values come from the 2021 article, rather than new runs.
Without history#
With history or export#
Camel and Spring Integration do not persist BPM instance state and do not resume failed runs after restart. They serve as a throughput reference for this hardware.
Checkpoints#
Average throughput since the beginning at each completion checkpoint. go-bpm uses the median run.
| System | 1k | 10k | 20k | 30k | 40k | 50k | 60k | 70k | 80k | 90k | 100k |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Without history | |||||||||||
| Apache Camel | 2 217 | 4 029 | 4 087 | 4 154 | 4 135 | 4 138 | 4 128 | 4 112 | 4 167 | 4 130 | 4 154 |
| Spring Integration | 1 996 | 3 757 | 3 859 | 3 891 | 3 898 | 3 899 | 3 888 | 3 900 | 3 914 | 3 922 | 3 910 |
| bpm-light, without export | 1 244 | 2 199 | 2 222 | 2 243 | 2 242 | 2 238 | 2 236 | 2 236 | 2 232 | 2 237 | 2 235 |
| Camunda, without history (H2 in memory) | 971 | 1 565 | 1 569 | 1 579 | 1 584 | 1 582 | 1 591 | 1 587 | 1 593 | 1 582 | 1 577 |
| go-bpm, methods, without history | 2 508 | 2 422 | 2 409 | 2 298 | 2 206 | 1 898 | 1 641 | 1 600 | 1 683 | 1 610 | 1 361 |
| go-bpm, service task, without history | 173 | 549 | 568 | 652 | 496 | 408 | 435 | 446 | 469 | 471 | 466 |
| Zeebe, without export | 100 | 112 | 117 | 118 | 119 | 119 | 118 | 118 | 119 | 117 | 117 |
| With history or export | |||||||||||
| bpm-light, export to H2 (file) | 1 105 | 1 943 | 1 970 | 1 976 | 1 978 | 1 960 | 1 967 | 1 957 | 1 972 | 1 963 | 1 960 |
| go-bpm, methods, history, async commit | 1 319 | 597 | 471 | 640 | 781 | 897 | 992 | 1 071 | 1 134 | 1 186 | 1 228 |
| go-bpm, methods, history | 1 422 | 1 119 | 591 | 759 | 887 | 973 | 1 016 | 1 064 | 1 109 | 1 139 | 1 153 |
| bpm-light, export to Elasticsearch | 307 | 900 | 1 107 | 1 105 | 1 103 | 1 099 | 1 070 | 1 089 | 1 105 | 1 030 | 1 102 |
| bpm-light, export to ClickHouse | 700 | 999 | 1 012 | 991 | 1 003 | 981 | 1 002 | 1 003 | 999 | 1 004 | 983 |
| go-bpm, service task, history, async commit | 575 | 191 | 305 | 371 | 406 | 454 | 513 | 572 | 637 | 696 | 752 |
| go-bpm, service task, history | 761 | 550 | 412 | 347 | 398 | 435 | 448 | 438 | 450 | 455 | 419 |
| Camunda, history in PostgreSQL | 279 | 346 | 302 | 331 | 306 | 319 | 296 | 309 | 303 | 296 | 293 |
| Zeebe, export to Elasticsearch | 88 | 106 | 112 | 114 | 113 | 115 | 113 | 113 | 113 | 113 | 112 |
Methodology#
The process is start → two REST calls → end. Each call requests static {} with a random ?v= parameter to avoid caching. The article used nginx; these runs use an HTTP stub inside the Go process.
- Methods: both calls run synchronously in one system queue step. A failure in the second call repeats the entire step.
- Service tasks: each call has a separate queued handler and retry budget, adding transaction overhead.
The actual benchmark methods and handlers:
// bench/internal/bpmgen/bench_methods/method_rest2.go
// Rest2 - метод задачи Rest2 «Вызов REST №2»
func Rest2(ctx context.Context, svc *services.Services, data *model.DataModel) error {
if err := svc.Rest.Call(ctx); err != nil {
return err
}
svc.Finished()
return nil
}// bench/internal/bpmgen/handlers/rest_call.go
// RestCall - обработчик service task с jobType "restCall" во всех процессах, где он встречается
// (job.ProcessName, job.ElementId). Вызывается асинхронно of очереди задач и при ошибке повторяется,
// поэтому должен быть идемпотентным (ключ - job.InstanceUid + job.ElementId)
func RestCall(ctx context.Context, svc *services.Services, job bpm.Job, data *model.DataModel) error {
if err := svc.Rest.Call(ctx); err != nil {
return err
}
if job.ElementId == "Rest2" {
svc.Finished()
}
return nil
}Eight goroutines start 100,000 instances through StartProcess; 16 system workers and 16 service task workers execute them. Each variant runs three times, clearing the dedicated benchmark database between runs.
GO_BPM_BENCH_DSN='postgres://postgres:postgres@localhost:5432/go_bpm_bench?sslmode=disable' ./run.shEnvironment: Intel(R) Core(TM) i7-4770K CPU @ 3.50GHz (8 threads), PostgreSQL 18.6 (Debian 18.6-1.pgdg13+2), go1.26.8, 2026-10-26; before and after runs, the database sustained from 5 534 to 36 620 disk-flushed commits per second. PostgreSQL 18 runs in Docker with host networking, shared_buffers=2GB and max_wal_size=4GB; remaining settings use defaults. synchronous_commit=on except where explicitly marked.
Interpretation and limitations#
The comparison systems were measured in 2021 with different Java, Camunda and Zeebe versions. Treat this as an order-of-magnitude comparison on the same hardware, not a current product ranking.
Disk commit throughput varied substantially between runs. Four CPU cores share starters, workers, the stub and PostgreSQL. Throughput changes after the starters finish enqueueing; charts therefore show medians and ranges.
The article used default PostgreSQL settings; go-bpm used the tuning above. With defaults and Docker port forwarding on this machine, method throughput was about 1,000–1,900 instances/s and service tasks about 350–500.
synchronous_commit=off acknowledges commits before WAL is flushed to disk. A database server crash can lose recent acknowledged transactions. Use that configuration only when the application's durability requirements permit it.
A bug found by the benchmark#
Early runs sometimes stalled for about 40 seconds at the end: a queue claim leased several tasks but delivered only one to a worker, leaving others waiting for lease expiry. This was fixed in d7d82c6. The results above include the fix.