Comparative benchmarks between syncx and the equivalent primitive from the
most widely used library for each problem in the Go ecosystem:
| Primitive | Compared against |
|---|---|
Semaphore |
golang.org/x/sync/semaphore (Weighted, used as a binary semaphore) |
Pool |
golang.org/x/sync/errgroup (SetLimit) |
CircuitBreaker |
github.com/sony/gobreaker/v2 |
Retry |
github.com/cenkalti/backoff/v4 |
This directory is its own Go module with its own go.mod, replaced to the
parent directory. That keeps these three external dependencies out of the
main syncx module — go get github.com/Ismahsantiago/syncx still pulls in
zero dependencies, since Go module tooling skips nested modules that declare
their own go.mod.
cd bench
go test -run=^$ -bench=. -benchmem ./...Add -count=10 and pipe through benchstat
for statistically sound comparisons before drawing conclusions:
go test -run=^$ -bench=. -benchmem -count=10 ./... > new.txt
go run golang.org/x/perf/cmd/benchstat@latest new.txtCaptured on an Apple M1 Pro (darwin/arm64, Go 1.25), -benchtime=1s -count=3, variance under 2% across runs. Numbers will differ on other
hardware — re-run locally rather than trusting these as absolute, but the
relative shape has been stable across repeated runs on this machine.
BenchmarkSyncxCircuitBreaker-8 27.0 ns/op 0 B/op 0 allocs/op
BenchmarkGobreakerCircuitBreaker-8 98.6 ns/op 0 B/op 0 allocs/op
BenchmarkSyncxRetry-8 3.1 ns/op 0 B/op 0 allocs/op
BenchmarkCenkaltiBackoffRetry-8 46.5 ns/op 48 B/op 3 allocs/op
BenchmarkSyncxSemaphore-8 152.8 ns/op 0 B/op 0 allocs/op
BenchmarkXSyncSemaphore-8 228.4 ns/op 175 B/op 2 allocs/op
BenchmarkSyncxSemaphoreTryAcquire-8 23.0 ns/op 0 B/op 0 allocs/op
BenchmarkXSyncSemaphoreTryAcquire-8 27.0 ns/op 0 B/op 0 allocs/op
BenchmarkSyncxSemaphoreMutexBaseline 128.0 ns/op 0 B/op 0 allocs/op
BenchmarkSyncxPool-8 594.7 µs/op 8.4 KiB 7 allocs/op (1000 jobs, 8 workers)
BenchmarkErrgroupPool-8 558.5 µs/op 47.2 KiB 2005 allocs/op (1000 jobs, 8 workers)
CircuitBreaker,Retry,Semaphore: syncx is faster and allocation-free where the comparison library allocates. This is mostly architectural, not a tuning trick —gobreakerandbackoffare built around interfaces and closures with more indirection per call, andx/sync/semaphore'sWeightedsupports arbitrary weights (a heap of waiters), which costs more than syncx's fixed-capacity channel when only ever acquiring 1. If you need weighted acquisition,x/sync/semaphoreis the right tool and this comparison does not apply.Poolvserrgroup: wall-clock time is a near tie — both are bound by the same 8-worker fan-out over 1000 trivial jobs, so scheduler overhead dominates either way. The real difference is allocations:Poolreuses a fixed set of worker goroutines and a shared results channel (7 allocations total for the whole run), whileerrgroup.Gospawns one goroutine per call (2005 allocations for 1000 jobs). Prefererrgroupwhen you just need bounded fan-out with first-error cancellation and no per-job result; preferPoolwhen you need aResultper job (value and error, paired with its input) streamed back as work completes.
None of this means "always use syncx" — it means syncx's primitives make narrower promises (fixed capacity, single output type, linear backoff) than their more general counterparts, and narrower promises are cheaper to keep. Pick based on the guarantees you actually need; see the root README for what each primitive promises.