Skip to content
Zever
Home

Benchmarks

Point-in-time micro-benchmarks for router, ORM, queue, and other hot paths, plus how to reproduce them.

19 min readEdit on GitHub

Measured numbers for router, ORM, and queue hot paths, plus how to re-run them. All benchmarks are deterministic (no network, no timing assertions) and live in *_bench_test.go files next to the code they measure. In-process backends (sqlite :memory:, miniredis) keep them offline; their absolute numbers are dominated by the backend’s own parsing, so compare shapes (allocs/op) and relative deltas, not raw ns.

Numbers are a point-in-time snapshot, not a performance contract: they depend on machine, CPU load, and Go version. Re-run locally before drawing conclusions, and treat committed numbers older than a few weeks as stale.

Every benchmark lives in a *_bench_test.go file next to the code it measures and follows these rules:

  • Deterministic: no network, no time.Sleep sync, no unseeded randomness, no timing assertions. In-process backends only (sqlite :memory:).
  • Allocation visibility: b.ReportAllocs() on every benchmark.
  • Setup excluded: b.ResetTimer() after warmup/setup; b.Cleanup for teardown (Open/Close).
  • Context: b.Context() (or b.Parallel’s implicit context), never a stored context.
  • Concurrent types: a b.RunParallel variant alongside the sequential one, with balanced work per iteration so memory stays flat.
  • Naming: Benchmark<Thing> for the hot path, Benchmark<Thing>Parallel for the concurrent variant, Benchmark<Thing>Before/After only when documenting an optimization.
  • Target: the operation the module exists to perform (for example cache Get/Set, queue Push/Pop, codec encode/decode, orm query build, dsl lex/parse).

Run all benchmarks with make bench (go test -bench=. -benchmem ./...) or per module with go -C <dir> test -run=NONE -bench=. -benchtime=1s -cpu=1,4 -benchmem.

Router (adapters/router/fiber, sequential vs concurrent ServeHTTP). ServeHTTP now runs the adaptor over a pooled *httptest.ResponseRecorder (recorderPool); BenchmarkServeHTTPAdaptor isolates the adaptor wrapping with a no-op handler. “before” is the pre-pool httptest.NewRecorder() path.

Benchmark ns/op B/op allocs/op
BenchmarkServeHTTP before (cpu=1) 5,973 6,638 60
BenchmarkServeHTTP after (cpu=1) 5,874 6,062 55
BenchmarkServeHTTP after (cpu=4) 4,855 6,079 56
BenchmarkServeHTTPAdaptor (cpu=1) 4,651 5,457 45
BenchmarkServeHTTPAdaptor (cpu=4) 3,622 5,461 45
BenchmarkServeHTTPParallel (cpu=1) 8,483 11,195 65
BenchmarkServeHTTPParallel (cpu=4) 4,001 11,219 65

The pool removes the recorder’s struct/map/buffer allocations from the hot path: ServeHTTP drops 5 allocs/op and ~576 B/op. BenchmarkServeHTTPAdaptor shows the adaptor wrapping alone costs ~45 allocs/op (the pooled recorder is 3 of them).

Router re-measurement (2026-10-14) and stdhttp default

Section titled “Router re-measurement (2026-10-14) and stdhttp default”

Re-ran the fiber benches on the same machine class after the newHTTPHandler rework (which mirrors gofiber/adaptor.FiberApp while skipping its unconditional body buffering and RemoteAddr resolution), plus the stdhttp benches for comparison. Medians of 3 runs, -benchtime 1s:

Benchmark ns/op (cpu=1) B/op allocs/op
fiber BenchmarkServeHTTP ~5,300 ~4,700 46
fiber BenchmarkServeHTTPAdaptor (no-op handler) ~3,700 ~4,080 35
fiber BenchmarkServeHTTPParallel ~8,200 / ~4,800 (cpu=4) ~9,800 55
stdhttp BenchmarkServeHTTPParam ~1,900 / ~1,500 (cpu=4) 2,072 17
stdhttp BenchmarkServeHTTPStatic ~1,100 / ~970 (cpu=4) 1,010 10
stdhttp BenchmarkServeHTTPParallel ~4,100 / ~3,100 (cpu=4) ~7,190 26

Takeaways:

  • stdhttp is the recommended default router (and the zever new scaffold default): the param path costs 17 allocs/op against fiber’s 46 (~2.7x fewer), and is ~3x faster per request on this machine. The gap is the fasthttpadaptor net/http conversion plus header/status/body merge-back, which every fiber request pays regardless of handler logic.
  • The older “45–55 allocs/op adaptor cost” note above is stale for the isolated adaptor: it now measures 35 allocs/op. The 45–55 range still brackets the full fiber ServeHTTP (46) through concurrent (55) paths.

Reproduce:

Terminal window
go test -run=NONE -bench='BenchmarkServeHTTP$|BenchmarkServeHTTPAdaptor|BenchmarkServeHTTPParallel' -benchtime=1s -cpu=1,4 -benchmem ./adapters/router/fiber/
go test -run=NONE -bench='BenchmarkServeHTTPParam|BenchmarkServeHTTPStatic|BenchmarkServeHTTPParallel' -benchtime=1s -cpu=1,4 -benchmem ./adapters/router/stdhttp/

ORM render (orm, sqlite :memory:). escapeLike and flattenGroupTerms were hoisted to a package-level replacer / a lazy dedup (linear scan for few leaves, map for many). “before” is the per-call strings.NewReplacer / always-map path.

Benchmark ns/op B/op allocs/op
BenchmarkEscapeLike before (cpu=1) 1,361 6,784 8
BenchmarkEscapeLike after (cpu=1) 51 24 1
BenchmarkEscapeLike after (cpu=4) 44 24 1
BenchmarkFlattenGroupTermsFew before (cpu=1) 434 572 7
BenchmarkFlattenGroupTermsFew after (cpu=1) 354 560 3
BenchmarkFlattenGroupTermsFew after (cpu=4) 369 560 3
BenchmarkFlattenGroupTermsRollup (cpu=1) 499 560 3
BenchmarkFlattenGroupTermsMany (cpu=1) 7,729 21,272 75

The hoisted replacer cuts escapeLike from 8 allocs to 1 (the result string). The lazy dedup cuts flattenGroupTerms for a few leaves from 7 allocs to 3 and is also faster (354 vs 434 ns); past the linear-scan threshold (linearDedupMaxLeaves = 8) it falls back to the map (...Many), the faster shape for many leaves.

Queue (adapters/queue/db, stale-claim sweep over sqlite). The sweep is one SELECT plus one set-based UPDATE for the whole batch: the database computes attempt = attempt + 1 itself (new orm expression assignment, orm.SetExpr(col, orm.Add(col.Expr(), 1))), under the same lease guard (claimed_by != '' AND claimed_until <= now AND id IN (...)), so a row settled between the SELECT and the UPDATE is skipped instead of double-bumped. “before” is the old N+1 loop (one guarded UPDATE per row).

Benchmark ns/op B/op allocs/op
BenchmarkReclaimStale before (cpu=1) 4,748,598 331,827 5,690
BenchmarkReclaimStale after (cpu=1) 645,270 109,471 3,208
BenchmarkReclaimStale before (cpu=4) 4,869,046 331,997 5,685
BenchmarkReclaimStale after (cpu=4) 631,722 109,574 3,210

The single set-based UPDATE cuts the sweep ~7.4x (4.75M ns/op to 645K ns/op at cpu=1): the N per-row round trips and per-row commit overhead collapse into one statement, and the attempt bump moves into the database. B/op drops ~3x (331 KB to 109 KB) and allocs/op ~1.8x (5,690 to 3,208) with it. A second run confirmed the after numbers within noise (646,554 / 637,319 ns/op).

Queue second pass (adapters/queue/db, stale-claim sweep over sqlite). The sweep collapsed from two statements to one: the batch is selected and released by a single set-based UPDATE whose IN subquery projects the same stale-claim set the standalone SELECT read (same lease guard, same id order, same batch LIMIT), so the ids slice, the per-row msgRow scan, and the SELECT-to-UPDATE window are gone. “before” is the two-statement sweep at commit 8a7f3d9. Medians of 10 runs (-count=10), benchstat p=0.000.

Benchmark ns/op B/op allocs/op
BenchmarkReclaimStale before (cpu=12) 898,500 109,534 3,204
BenchmarkReclaimStale after (cpu=12) 405,700 10,834 130

The single set-based UPDATE cuts the sweep a further ~2.2x (898.5µs to 405.7µs): the SELECT’s per-row driver work and the ids slice disappear, and the subquery plus outer guard evaluate atomically, so a row settled mid-sweep is still skipped rather than double-bumped. B/op drops ~10x and allocs/op ~24x (3,204 to 130). One tradeoff: an idle sweep now issues a 0-row UPDATE instead of skipping the write entirely (~52µs per sweep at the 250ms gate, ~0.2% CPU).

Rate limiter (adapters/ratelimit/memory, striped lock table). The bucket map was sharded into 64 stripes (DefaultStripeCount) keyed by an FNV-1a hash of the limiter key, each with its own sync.RWMutex. The global MaxEntries bound is preserved exactly: new-key admission takes a short admitMu to reserve a slot under the bound (evicting idle-first, then least-recently-used, across stripes) before inserting into the stripe, so existing-key calls never touch admitMu. “before” is the single sync.RWMutex table at commit 9e3a0d0. Medians of 3 runs, -benchtime 2s.

Benchmark ns/op B/op allocs/op
BenchmarkAllowParallel before (cpu=1) 86.57 0 0
BenchmarkAllowParallel after (cpu=1) 88.08 0 0
BenchmarkAllowParallel before (cpu=4) 122.2 0 0
BenchmarkAllowParallel after (cpu=4) 40.70 0 0
BenchmarkAllowParallelHotKey before (cpu=1) 75.00 0 0
BenchmarkAllowParallelHotKey after (cpu=1) 76.21 0 0
BenchmarkAllowParallelHotKey before (cpu=4) 112.4 0 0
BenchmarkAllowParallelHotKey after (cpu=4) 114.6 0 0

Sharding cuts the many-keys parallel path ~3x at cpu=4 (122.2 to 40.70 ns/op): before, every Allow serialized on one lock and cpu=4 was slower than cpu=1. The single-key benchmark is unchanged (75.00 to 76.21 ns/op at cpu=1) because one key still serializes on its stripe; sharding helps key parallelism, not same-key throughput. Allocations stay at 0 B/op, 0 allocs/op on every row.

Reproduce:

Terminal window
go test -run '^$' -bench 'BenchmarkAllowParallel' -benchtime 2s -cpu 1,4 -count 3 ./adapters/ratelimit/memory/

Idempotency (adapters/idempotency/redis, miniredis). Begin and Complete are now one Lua script each (1 RTT) instead of two client calls. Absolute numbers are dominated by miniredis’s in-process command parsing.

Benchmark ns/op B/op allocs/op
BenchmarkBegin (cpu=1) 152,587 190,137 786
BenchmarkBegin (cpu=4) 128,635 190,249 786
BenchmarkComplete (cpu=1) 162,142 198,666 793
BenchmarkComplete (cpu=4) 133,553 198,746 793

Shared Redis registry (shared/redisclient, offline, 2026-10-04). Shared now returns a refcounted client keyed by the resolved addr+DB+TLS/password identity, so every Redis-backed battery on the same Redis shares one client/pool. BenchmarkSharedAcquireRelease is a warm registry hit against a pre-seeded entry; BenchmarkPrivateClientBuild is the per-battery goredis.NewClient the registry avoids on every borrower after the first (idle-conn warming disabled to stay offline). Medians of 3 runs, -benchtime 1s.

Benchmark ns/op B/op allocs/op
BenchmarkSharedAcquireRelease (cpu=1) 343 656 5
BenchmarkSharedAcquireRelease (cpu=4) 306 656 5
BenchmarkPrivateClientBuild (cpu=1) 10,155 8,786 71
BenchmarkPrivateClientBuild (cpu=4) 9,853 18,215 71

A shared hit is ~30x faster and ~14x fewer allocs than building a private client (343 vs 10,155 ns/op, 5 vs 71 allocs/op at cpu=1): later borrowers skip the client/pool construction entirely. The shared hit still allocates its own *goredis.Options and key string per call, dwarfed by the pool build they replace.

Job worker (core/job, empty-poll backoff). runLoop used to call time.After(wait) on every empty poll, allocating a fresh timer each time; it now creates one *time.Timer for the run and Resets it per poll, stopping it on exit. “before” is the time.After path. Medians of 5 runs.

Benchmark ns/op B/op allocs/op
BenchmarkWorkerEmptyPoll before (cpu=1) 379.0 248 3
BenchmarkWorkerEmptyPoll after (cpu=1) 241.7 0 0
BenchmarkWorkerEmptyPoll before (cpu=4) 335.0 248 3
BenchmarkWorkerEmptyPoll after (cpu=4) 237.8 0 0

Reusing the timer removes the 3 allocs/op (timer, channel, runtime timer) and 248 B/op from every idle poll, and cuts the wait path ~1.6x at cpu=1 (379.0 to 241.7 ns/op). Backoff values are unchanged: the first poll in a run waits 0, then 1ms, 2ms, 4ms, …, capped at DefaultMaxPollWait.

ORM render group dedup (orm/render, sqlite). dedupGroupLeaves’s map fallback keyed on a formatted string per leaf; it now keys on a comparable groupLeafKey{fn, col} struct, removing the per-leaf key allocation. Medians of 3 runs, cpu=12.

Benchmark ns/op B/op allocs/op
BenchmarkFlattenGroupTermsMany before 6,164 21,272 75
BenchmarkFlattenGroupTermsMany after 5,598 22,168 11

The struct key drops 64 allocs/op (one per leaf) on a 64-leaf GROUP BY and cuts ns/op ~9%; B/op rises slightly (keys stored inline) but total allocation count, and thus GC pressure, falls sharply.

Container resolve/close (container, sqlite shared-pool open+close). The shutdown-critical subset (DB, Cache, Queue, Scheduler, Job) is resolved from a cold container and closed. Close now reuses one buffered result channel across successful closes (reallocating only after a timeout abandons a goroutine), runs the shutdown body as a top-level closeAsync (no closure per go), and dedups with a local plain map created lazily. logOpen also drops its intermediate [...] string concatenation. Medians of 5 runs, cpu=12.

Benchmark ns/op B/op allocs/op
BenchmarkContainer_ResolveSubset before 11,090 11,157 57
BenchmarkContainer_ResolveSubset after 10,884 10,289 37

The close-path rework drops 20 allocs/op (~35%): the per-service result channel and goroutine closure disappear, sync.Map’s per-key entry nodes are replaced by one small map, and the pool log line skips a string concat. ns/op is dominated by config.Default() and the fresh Container, so the delta is modest; B/op falls ~870 bytes/op.

SMTP buildMIME (adapters/mailer/smtp). The header block now writes precomputed strings through buf.Write/WriteString and grows once via estimateSize (was fmt.Fprintf per header, unbounded growth). writeBase64 encodes into a preallocated []byte and writes the line and CRLF separately (was encoded[i:end]+"\r\n" per 76-byte line). joinAddresses builds into a strings.Builder (single-address fast path) instead of a []string plus strings.Join. The generated MIME bytes are unchanged. Medians of 10 runs, cpu=12.

Benchmark ns/op B/op allocs/op
BenchmarkBuildMIME before 8,206 5,892 102
BenchmarkBuildMIME after 6,711 6,421 90
BenchmarkBuildMIMEAttachments before 11,812 9,799 155
BenchmarkBuildMIMEAttachments after 9,332 6,933 135

The rework cuts allocs/op ~12% (102 to 90, 155 to 135) and ns/op ~19-21%. B/op rises ~530 bytes/op on BenchmarkBuildMIME (the estimateSize hint over-reserves header slack, still one buffer allocation) and falls ~2.9 KB/op on BenchmarkBuildMIMEAttachments (the old growth path reallocated the buffer ~7 times; the encoded attachment now lives in one preallocated slice).

Mailer log (adapters/mailer/log, io.Discard, 2026-10-05). Send reuses one logMessage (with its address/attachment slices) across calls under the existing mutex, encodes into a reused bytes.Buffer via json.MarshalWrite (struct passed by pointer so encoding/json/v2 skips its reflect.New shallow copy and bytes.Clone result copy), and writes the buffer plus the newline byte in a single Write. mailer.Address.Validate now splits on @ and scans domain labels with strings.IndexByte instead of strings.SplitN/strings.Split, dropping its per-call slices. The checker constructor allocates reusable scratch state once (288 B); the per-send path no longer allocates. “before” is the json.Marshal + append(data, '\n') path. Medians of 10 runs.

Benchmark ns/op B/op allocs/op
BenchmarkSend before (cpu=1) 2,034 1,088 12
BenchmarkSend after (cpu=1) 1,462 0 0
BenchmarkSendParallel before (cpu=1) 2,280 1,090 12
BenchmarkSendParallel after (cpu=1) 1,599 0 0

Send drops 12 allocs/op to 0 and ~1 KiB/op to 0, and is ~28% faster at cpu=1 (2,034 to 1,462 ns/op); the concurrent variant tracks it. The per-call work was six allocs from Address.Validate (SplitN+Split per recipient), two address slices, one attachment slice, and the codec’s reflect.New + bytes.Clone + append growth. All are now reused or eliminated; output bytes are unchanged.

Reproduce:

Terminal window
go test -run '^$' -bench=. -benchmem -count=10 ./adapters/mailer/log/

TUI closest matcher (cmd/zever, 2026-10-05). closest used to allocate a full (la+1)x(lb+1) DP matrix per candidate (21 top-level commands), and damerauLevenshtein allocated the same matrix per call. The kernel now keeps three rolling rows in one scratch buffer (damerauLevenshteinScratch) that closest sizes to the longest candidate and reuses across the whole scan; damerauLevenshtein delegates with a nil scratch. Distances, the chosen match, and ordering are unchanged. Medians of 10 runs, cpu=12.

Benchmark ns/op B/op allocs/op
BenchmarkClosest before 8,611 14,008 176
BenchmarkClosest after 5,429 416 1
BenchmarkDamerauLevenshtein before 597 832 9
BenchmarkDamerauLevenshtein after 413 240 1

The scratch rows cut BenchmarkClosest from 176 to 1 alloc/op (-99.4%) and 14,008 to 416 B/op (-97.0%), and ~37% faster (8.6µs to 5.4µs); the standalone kernel drops 9 to 1 allocs/op, ns/op within noise. The remaining alloc is the single scratch slice per closest call.

Search (adapters/search/db, sqlite file-backed, 2026-10-05). IndexBatch now writes the whole batch as one multi-row INSERT ... ON CONFLICT (id, idx) DO UPDATE SET content = excluded.content, metadata = excluded.metadata (both dialects expose the proposed row as excluded), instead of one typed upsert per document, through the db.Preparer probe so repeated batch shapes reuse the cached statement. Search preallocates the hits slice at limit capacity and aliases the count query’s arg slice to the hits args’ prefix. “before” is the per-document orm loop / append-grown hits path. Medians of 10 runs.

Benchmark ns/op B/op allocs/op
BenchmarkIndexBatch before 630,000 28,688 553
BenchmarkIndexBatch after 132,800 8,951 182
BenchmarkSearch before 379,700 11,170 253
BenchmarkSearch after 372,400 10,378 244

The multi-row upsert cuts IndexBatch ~4.7x (630 to 133 ns/op): the eight per-document orm chains collapse into one statement, and allocs/op drop 67% (553 to 182) with B/op down 69% (28.0 to 8.7 KiB). The remaining per-document work is the metadata map clone plus JSON encode. Search drops 9 allocs/op (253 to 244) and ~790 B/op; its ns/op is dominated by the two FTS5 queries and the per-hit JSON metadata decode, so the time delta is within noise.

Vectorstore brute-force query (adapters/vectorstore/db, sqlite leg, 2026-10-05). 256-row scan with topK=10. decodeRow used to JSON-decode metadata for every scanned row, but only the topK survivors leave the bounded heap (~246 wasted decodes per query). The heap item now carries the raw metadata blob, heapToSorted decodes metadata for the topK survivors only, and the heap is preallocated with a bounded constant capacity. “before” is the per-row decode path. Medians of 10 runs, cpu=12.

Benchmark ns/op B/op allocs/op
BenchmarkQuery before 506,500 181,030 4,919
BenchmarkQuery after 260,500 84,930 3,439

Lazy decode cuts allocs/op ~30% (4,919 to 3,439) and ns/op ~49% (506.5µs to 260.5µs): each skipped decode was a map[string]any plus a JSON parse, and the decode dominated the scan. B/op falls ~53% (176.8 KiB to 82.9 KiB). Semantic note: corrupt metadata in a row that does not survive topK no longer fails the query (TestEdgeQuery_corruptMetadataBelowTopK documents this); a surviving row with corrupt metadata still fails.

Config redaction + env overlay (config, 2026-10-05). Three semantics-preserving changes. (1) isSensitiveKey no longer allocates a strings.ToLower copy per key: already-lowercase ASCII keys match directly, mixed-case ASCII keys fold in place (asciiContainsFold/asciiEqualFold), and only non-ASCII keys fall back to strings.ToLower. (2) Redact merges the deep copy and the reclamation walk into a single pass (redactCopyMap); the public contract (fresh copy, caller map never mutated) is unchanged. (3) RedactedServices redacts the freshly built serviceToMap output in place via redactMap. Separately, applyEnv matches the head case-insensitively against the 34 known service names without allocation (matchService, first-byte candidate groups) and lowers the rest only on a match, and findField compares normalized field names without allocation (normEqualFold). Redaction output is byte-identical to the old implementation. Medians of 10 runs, cpu=12.

Benchmark ns/op before ns/op after allocs/op before allocs/op after
BenchmarkRedactedServices 112,600 74,060 597 539
BenchmarkLoadFile 64,380 39,170 572 202
BenchmarkApplyEnv 27,335 5,419 374 2
BenchmarkRedact 1,783 1,239 12 10

ApplyEnv drops 374 allocs/op to 2 (the os.Environ slice and the one lowered field name) and is ~5x faster; LoadFile inherits most of that (572 to 202 allocs/op, ~39% faster). RedactedServices is ~34% faster; its remaining allocs/op are the JSON marshal/unmarshal bridge in serviceToMap, kept marshal-based on purpose to preserve each package’s json-tag semantics. Redact drops 2 allocs/op and ~31% ns/op from the merged pass.

DSL lexer (dsl/lexer, 2026-10-05). scanIllegal builds the diagnostic message and the ILLEGAL token literal into one stack buffer and materializes them as a single string. The rune to (msg, lit) string is a pure function, so it is now memoized in a per-Lexer cache (illegalCache, 8 entries): repeated illegal runes share one backing array, cutting the per-rune string alloc to zero on hits. The diag.Diagnostic alloc is unavoidable (each is retained in Lexer.errs). Other token literals (ident, number, comment text) are at floor: each needs its own string. Medians of 10 runs, -count=10.

Benchmark ns/op B/op allocs/op
BenchmarkLexIllegal before (cpu=12) 1,885,509 1,471,879 18,019
BenchmarkLexIllegal after (cpu=12) 733,141 1,183,961 9,022
BenchmarkLexAstral before (cpu=12) 84,848 75,405 1,015
BenchmarkLexAstral after (cpu=12) 38,682 59,437 516

The memoization cuts BenchmarkLexIllegal ~2.6x (1.89M to 733K ns/op) and ~50% of allocs/op (18,019 to 9,022, the remaining 9,000 being the retained Diagnostic structs); BenchmarkLexAstral tracks it (1,015 to 516 allocs/op, ~55% faster). Token output is byte-identical: same tokens, diagnostics, and positions (golden fixtures under dsl/compile/testdata unchanged).

Reproduce:

Terminal window
go test -run '^$' -bench='BenchmarkClosest|BenchmarkDamerauLevenshtein' -benchmem -count=10 ./cmd/zever/
go test -run '^$' -bench='BenchmarkQuery$' -benchmem -count=10 ./adapters/vectorstore/db/
go test -run '^$' -bench=. -benchmem -count=10 ./adapters/search/db/
go test -run '^$' -bench=. -benchmem -count=10 ./config/
go test -run '^$' -bench='BenchmarkLexIllegal|BenchmarkLexAstral' -benchmem -count=10 ./dsl/lexer/

Router and queue are fast, so they run 1s per bench; ORM runs 100 iterations per bench:

Terminal window
go test -run=NONE -bench=. -benchtime=1s -cpu=1,4 -benchmem ./adapters/router/fiber/
go test -run=NONE -bench=BenchmarkEscapeLike -benchtime=1s -cpu=1,4 -benchmem ./orm/
go test -run=NONE -bench=BenchmarkFlattenGroupTerms -benchtime=1s -cpu=1,4 -benchmem ./orm/render/
go test -run=NONE -bench=BenchmarkReclaimStale -benchtime=1s -cpu=1,4 -benchmem ./adapters/queue/db/
go test -run=NONE -bench=. -benchtime=1s -cpu=1,4 -benchmem ./adapters/idempotency/redis/
go test -run=NONE -bench='BenchmarkSharedAcquireRelease|BenchmarkPrivateClientBuild' -benchtime=1s -cpu=1,4 -benchmem ./shared/redisclient/
go test -run=NONE -bench=BenchmarkWorkerEmptyPoll -benchtime=1s -cpu=1,4 -benchmem ./core/job/
go test -run=NONE -bench=BenchmarkContainer_ResolveSubset -benchtime=200ms -count=5 -benchmem ./container/
go test -run=NONE -bench=. -benchtime=100x -cpu=1,4 -benchmem ./orm/

Each path is its own Go module; run from the repo root with the go.work workspace active (or go -C <dir> test ... from inside the module). -run=NONE skips functional tests so only benchmarks execute.

  • CPU: 11th Gen Intel(R) Core(TM) i5-11400H @ 2.70GHz, 6 cores / 12 threads
  • Caches: L1d 288 KiB, L1i 192 KiB, L2 7.5 MiB, L3 12 MiB
  • RAM: 16 GiB (15 GiB usable)
  • OS/Go: linux/amd64, go version go1.27.0 linux/amd64
  1. Check out the same commit and ensure go.work resolves (go env GOWORK).
  2. Run the commands above; each prints goos/goarch/cpu lines identifying the machine.
  3. Compare with benchstat across runs (-count=5) before claiming a regression or win; single-run ns/op differences under ~5% are noise.

Concurrent-load (b.RunParallel) coverage:

  • Router: BenchmarkServeHTTPParallel (adapters/router/fiber/fiber_bench_test.go).
  • ORM: BenchmarkPreloadConcurrent (orm/preload_bench_test.go).
  • Queue: BenchmarkMemoryRoundTripParallel (adapters/queue/memory/memory_bench_test.go).
  • Rate limiter: BenchmarkAllowParallel / BenchmarkAllowParallelHotKey (adapters/ratelimit/memory/memory_bench_test.go).
  • Mailer log: BenchmarkSendParallel (adapters/mailer/log/log_bench_test.go).

Adaptor-isolation coverage:

  • Router: BenchmarkServeHTTPAdaptor (adapters/router/fiber/fiber_bench_test.go).

Round-trip coverage (in-process backends):

  • Queue: BenchmarkReclaimStale (adapters/queue/db/queue_bench_test.go).
  • Idempotency: BenchmarkBegin / BenchmarkComplete (adapters/idempotency/redis/redis_bench_test.go).
  • Shared Redis registry: BenchmarkSharedAcquireRelease / BenchmarkPrivateClientBuild (shared/redisclient/shared_test.go).
  • Job worker: BenchmarkWorkerEmptyPoll (core/job/worker_bench_test.go).
  • Vectorstore: BenchmarkQuery (adapters/vectorstore/db/pgvector_bench_test.go).

Render-coverage (ORM):

  • BenchmarkEscapeLike (orm/column_bench_test.go).
  • BenchmarkFlattenGroupTermsFew / ...Rollup / ...Many (orm/render/agg_bench_test.go).

Startup-coverage (CLI):

  • BenchmarkHelpStartup (cmd/zever/bench_test.go), budget TestHelpStartupBudget (200ms) + TestHelpStartupAllocs (1500 allocs) in cmd/zever/cli_perf_test.go.

Benchmarks use b.Context(); helpers assert t.Helper where applicable.

Release-style build (go build ./cmd/zever, Go 1.27.2, linux/amd64): 309 MB on disk. Process cold start (--help to exit): ~420 ms first run (page cache cold), ~60–90 ms warm; in-process --help (BenchmarkHelpStartup) is ~99 µs/op, 69 KB/op, 625 allocs/op — binary load and package init dominate, not Cobra dispatch. GODEBUG=inittrace=1 shows ~700 init functions but negligible init clock time; the bulk of wall time before main is mapping the binary.

go tool nm -size top contributors by symbol size: third-party adapter/UI dependencies (github.com/... group, ~60 MB: charm UI stack, sqlite driver via modernc.org/...), Go FIPS crypto tables (~33 MB crypto/internal/fips140/drbg), and codegen/grpc (google.golang.org/..., go/types). The CLI already links only the zero-infra adapter set (cmd/zever/adapters.go: memory/log/noop/stdhttp); the weight is transitive (embedded sqlite, TUI, telemetry), not registered adapters.

Repro: go build -o /tmp/zever ./cmd/zever && ls -lh /tmp/zever, GODEBUG=inittrace=1 /tmp/zever --help 2>&1 | grep '^init ' | wc -l, go tool nm -size /tmp/zever | awk 'NF==4 {m[$4]+=$2} END {for (p in m) printf "%d %s\n", m[p], p}' | sort -rn | head.

Slim CLI (zever build --adapters, 2026-10-14)

Section titled “Slim CLI (zever build --adapters, 2026-10-14)”

zever build --adapters=a,b compiles a slim CLI registering only the selected adapters (bare battery = every known adapter for it, battery/adapter = one pin; empty = full CLI). Unselected adapter packages leave the binary via a go build -overlay of the central registration list (cmd/zever/adapters.go), so no build-tagged source variants are committed — explicit list filtering, not tags. Unknown names fail listing the choices. The doctor adapters meta-check is scoped to the project’s enabled batteries, so a slim binary passes its own doctor for its project (smoke-check with <out> doctor).

Measured on this machine (Go 1.27.2, linux/amd64, warm page cache, medians of 5 --help process spawns per session; wall time varies with load, so ranges across two sessions):

Binary Size Warm --help
Full CLI (go build ./cmd/zever) 309.7 MB ~40–56 ms
Slim floor (--adapters=log,router: 4 log + 2 router adapters) 63.3 MB ~27–38 ms

The floor build is ~80% smaller (~5x) and ~32% faster to start; the saving is the excluded adapters’ transitive dependencies (pgx, go-redis, fasthttp, otel, cloud SDKs). In-process --help (BenchmarkHelpStartup, ~99 µs/op) is unchanged — the delta is binary mapping and package init, not dispatch.

Repro: go build -o /tmp/zever-full ./cmd/zever && /tmp/zever-full build --adapters=log,router --out /tmp/zever-slim && ls -lh /tmp/zever-full /tmp/zever-slim, then printf 'LOG_ADAPTER=slog\nROUTER_ADAPTER=stdhttp\n' > /tmp/slimdemo/.env && /tmp/zever-slim doctor (exit 0, all OK).