Production
Performance
Benchmark surfaces and local result boundaries.
Performance
Nidus aims to keep the framework layer visible and measurable instead of hiding cost behind broad throughput claims. The benchmark suite is intentionally split into dependency resolution, routing composition, and request lifecycle targets so changes can be compared at the right boundary.
Run the full local benchmark surface with:
cargo bench --bench dependency_resolution
cargo bench --bench configuration
cargo bench --bench routing
cargo bench --bench request_lifecycle
cargo bench --bench event_bus
cargo bench --bench integration_hot_paths
cargo bench -p nidus-cache --bench cache_hot_paths
For a quick smoke run with reduced Criterion sampling:
cargo bench --bench dependency_resolution -- --warm-up-time 0.1 --measurement-time 0.2 --sample-size 10
cargo bench --bench configuration -- --warm-up-time 0.1 --measurement-time 0.2 --sample-size 10
cargo bench --bench routing -- --warm-up-time 0.1 --measurement-time 0.2 --sample-size 10
cargo bench --bench request_lifecycle -- --warm-up-time 0.1 --measurement-time 0.2 --sample-size 10
cargo bench --bench event_bus -- --warm-up-time 0.1 --measurement-time 0.2 --sample-size 10
cargo bench --bench integration_hot_paths -- --warm-up-time 0.1 --measurement-time 0.2 --sample-size 10
cargo bench -p nidus-cache --bench cache_hot_paths -- --warm-up-time 0.1 --measurement-time 0.2 --sample-size 10
Homelab End-to-End Campaign (2026-07-15)
The versioned website benchmark now includes a fresh Kubernetes campaign for the 1.0.12 candidate at source commit c0b6d82e9649, paired in the same run with a control built from the exact v1.0.4 tag at ddbfd3e29bfd. FastAPI, Spring Boot, and Express implemented the same PostgreSQL-backed contract. The original June 30, 2026 1.0.4 website snapshot is preserved as historical data, but it is not used for the deltas below because its raw repetition series was not retained.
The base matrix covered five stacks and five profiles with three rotated repetitions: 8 VUs, a 5-second warmup, a 20-second measured window, 50 ms think time for focused profiles, and 200 ms for mixed. Apps used one replica, 1 CPU, and 512 MiB. PostgreSQL used 1 CPU and 1 GiB with its 512 MiB benchmark data volume memory-backed to keep application writes off the control-plane disk. The campaign retained 75 base measurements plus 12 adaptive repeatability measurements, for 87 measured cells and 179 raw k6 summaries. No benchmark source or harness code is stored in this repository.
| Profile | Samples 1.0.4 / 1.0.12 | 1.0.4 req/s | 1.0.12 req/s | Req/s change | 1.0.4 average | 1.0.12 average | Average change | 1.0.4 p95 | 1.0.12 p95 | p95 change |
|---|---|---|---|---|---|---|---|---|---|---|
| Ping | 3 / 3 | 156.31 | 156.35 | +0.03% | 405.14 us | 414.79 us | +2.38% | 645.69 us | 684.29 us | +5.98% |
| Users | 3 / 3 | 300.76 | 301.26 | +0.17% | 1.20 ms | 1.17 ms | -2.06% | 1.77 ms | 1.79 ms | +0.95% |
| Projects | 3 / 3 | 442.29 | 443.34 | +0.24% | 1.16 ms | 1.11 ms | -4.39% | 1.70 ms | 1.67 ms | -1.66% |
| Events | 3 / 3 | 300.51 | 300.57 | +0.02% | 1.23 ms | 1.23 ms | +0.42% | 1.85 ms | 1.78 ms | -3.46% |
| Mixed | 6 / 3 | 230.63 | 231.55 | +0.40% | 1.19 ms | 0.99 ms | -16.89% | 1.88 ms | 1.71 ms | -8.89% |
Positive throughput change is better; negative latency change is better. All ten Nidus candidate/control groups passed the predeclared repeatability limits: 5% throughput CV, 15% average-latency CV, and 20% p95 CV. The campaign is published as qualified, not strictly accepted, because Spring ping average latency ended at 15.58% CV against the 15% limit after the policy maximum of nine retained samples. No sample was discarded and no threshold changed. All stacks still completed with 0% HTTP failures and 100% checks.
This is a paced end-to-end workload, not a maximum-throughput or capacity claim. The complete cross-framework tables, raw summaries, integrity checks, sanitized runtime identities, and evidence manifest are available on the benchmark page and in the machine-readable result set. The benchmark candidate is the code now released as 1.0.12; only documentation and release metadata changed after the measured source commit.
Coverage
The current benchmark surface covers:
- singleton dependency resolution, including first construction
- typed configuration deserialization with 128 service entries
- required typed configuration lookup across a six-segment path
- module-graph validation with 128 feature modules and visible providers
- ordered lifecycle startup with 32 hooks
- raw Axum route composition
- Nidus controller route composition
- multi-route Nidus controller construction
- raw Axum baseline request handling
- hello-world request handling and app construction
- controller plus injected service request handling and app construction
- controller setup
- guard middleware
- validation extraction
- request-scoped dependency resolution through HTTP, including the public
- request context cloning
- per-layer middleware: security headers, body limit, legacy request ID,
- rate limit store check with 10,000 tracked identities
- trusted-proxy identity extraction and extractor cloning
- structured logging span creation with request and trace headers
- case-insensitive structured-logging header-redaction lookup
- bounded event publication at a full 10,000-event subscriber capacity, plus
- production default stack with and without in-process metrics
- constructing an OpenAPI document with 64 distinct schemas
- separately constructing 8- and 100-route OpenAPI documents, constructing a
- serving a 100-route OpenAPI document
- Prometheus metrics record-response, record-error, and render-text paths
- shared integration envelope serialization/deserialization at a 1 KiB payload
- durable job validation/construction and retry-bound calculation
- lifecycle, adapter, and event observability recording with repeated bounded
- Moka cache hits with and without a configured key namespace
RequestScoped<T> extractor
validated request ID, request context, error envelope, panic catching, timeout response, and allowed/rejected/store-error rate limit paths
one- and four-subscriber fan-out controls
100-route document from generated metadata, and rendering 100 routes
labels
The integration benchmark uses a 5% Criterion noise threshold. A change beyond that bound must be explained or reverted before release; smaller movement is reported but is not treated as a regression, particularly for the sub-2 ns retry arithmetic row.
The request lifecycle benchmark includes equivalent raw Axum request and routing composition baselines where they are meaningful. Other rows are microbenchmarks for specific framework behavior and should be compared to their own history.
Local Results
Shared typed subgraphs and reproducible feature checks (2026-07-27)
ModuleGraph::from_root_and_modules previously created a fresh typed-discovery set for the root and for every explicit definition. Two explicit feature modules that imported the same typed dependency therefore emitted that dependency twice and failed with DuplicateModule. Discovery now shares one set across the whole graph while still forwarding caller-supplied top-level duplicates to the existing validator.
The focused test was added before the implementation. On the untouched source, this command passed four controls and failed the shared-diamond case with DuplicateModule { module: "SharedExplicitDependencyModule" }:
cargo test --locked -p nidus-core --test app bootstrap_with_modules
After the fix, all five focused cases pass. They prove that the shared module is present once, its provider registrar and async initializer each run once, the initializer runs before both importers, duplicate explicit definitions still fail, and an explicit definition already reached through the typed root still fails. The existing startup-only graph control was:
CARGO_TARGET_DIR=/tmp/nidus-shared-subgraph-candidate \
cargo bench --locked --bench dependency_resolution -- \
'nidus 128-module graph validation' \
--warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot
It measured 43.366-43.770 us, within 5% of the earlier 42.078-42.424 us result. The successful shared graph has no equivalent baseline because the baseline rejects it, so this is reliability hardening, not a graph-construction speedup claim.
The integration feature matrix now runs every cargo check with --locked, matching its existing locked dependency-tree assertions. The complete matrix passed, and the Cargo.lock SHA-256 remained 9be4f2c0258fefc18bad229b660de25eea393e40f6774a7f675bfe5d66eb033b:
bash scripts/check-integration-feature-matrix.sh
git diff --exit-code -- Cargo.lock
This is deterministic CI hardening; it is not a compile-time improvement.
An extractor-specific request benchmark was also added before considering an Arc clone removal:
CARGO_TARGET_DIR=/tmp/nidus-request-scoped-extractor-20260727 \
cargo bench --locked --bench request_lifecycle -- \
'nidus request-scoped (extractor )?route' \
--warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot \
--save-baseline before-request-scope-ready-20260727
CARGO_TARGET_DIR=/tmp/nidus-request-scoped-extractor-20260727 \
cargo bench --locked --bench request_lifecycle -- \
'nidus request-scoped (extractor )?route' \
--warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot \
--baseline before-request-scope-ready-20260727
No runtime source changed between those two runs, yet the direct-resolution control moved from 838.38-864.13 ns to 782.29-797.03 ns and the real extractor moved from 841.66-863.98 ns to 796.13-803.54 ns. Criterion reported approximately 8.6% common-mode improvement for unchanged code. A std::future::ready rewrite was therefore rejected: the current RPIT future is already unboxed, eager resolution would change factory side-effect timing, and the measured environment cannot resolve the smaller refcount hypothesis.
The complete primary-source review and candidate rejection matrix are in the 2026-07-27 follow-up.
Typed config path, static request identity, and module-order pass (2026-07-27)
Typed nested configuration reads previously cloned every path segment into a String, collected those strings into a Vec, joined them into an error label, and then traversed the collected path. The typed methods now traverse the stored JSON value while building the same full label in one pass. Traversal continues consuming label segments after a lookup miss, preserving exact missing-value and deserialization paths. Object, array, empty-path, empty-segment, scalar, invalid-index, and one-shot-iterator tests cover that contract. The raw get_path method and public typed method signatures are unchanged.
Framework-owned "anonymous" rate-limit identities also created a temporary owned String per request. RequestIdentity now keeps its private value as Cow<'static, str>: the public new(impl Into<String>) constructor remains exactly the same and still creates an owned identity, while private framework fallbacks borrow the static label. Tests inspect owned and borrowed variants, clone behavior, equality, hashing, exact fallback text, and shared in-memory-rate-limit windows. Dynamic header, context, and IP identities remain owned.
The initial benchmark harness was added before either implementation and run against the untouched ab1c89d66e61a4a1a58d116290ca878fbcca72d9 baseline. Both sides used rustc 1.96.0 on aarch64-apple-darwin, 150 samples, a two-second warm-up, and a five-second measurement:
CARGO_TARGET_DIR=/tmp/nidus-20260727-bench-target \
cargo bench --locked --bench configuration -- \
'nidus config required typed path 6 segments' \
--warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot \
--save-baseline pre-config
CARGO_TARGET_DIR=/tmp/nidus-20260727-bench-target \
cargo bench --locked --bench request_lifecycle -- \
'nidus middleware rate limit' \
--warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot \
--save-baseline pre-rate
# Repeat from the candidate checkout with --baseline pre-config/pre-rate.
The baseline-first Criterion comparison produced:
| Benchmark | Baseline | Candidate | Criterion change |
|---|---|---|---|
| Required typed path, 6 segments | 178.41-180.10 ns | 112.90-114.38 ns | -37.606% to -36.729% |
| Rate limit, allowed | 867.29-873.52 ns | 794.32-799.62 ns | -9.0902% to -7.8581% |
| Rate limit, rejected | 845.27-850.83 ns | 776.61-781.36 ns | -8.4633% to -7.5913% |
| Rate-limit store error | 886.45-895.02 ns | 808.39-814.82 ns | -9.4789% to -8.5118% |
All four comparisons reported p = 0.00. A reverse-order run compiled the candidate and detached baseline into independent target directories. A shared-target attempt was discarded because Cargo reused the candidate benchmark binary across worktree roots without rebuilding it.
| Benchmark | Candidate first | Baseline second | Point-estimate change |
|---|---|---|---|
| Required typed path, 6 segments | 116.26 ns | 168.90 ns | -31.17% |
| Rate limit, allowed | 807.47 ns | 834.50 ns | -3.24% |
| Rate limit, rejected | 779.79 ns | 800.77 ns | -2.62% |
| Rate-limit store error | 813.16 ns | 833.39 ns | -2.43% |
The config improvement clears the repository's 5% threshold in both execution orders and is retained as a focused typed-read latency and intermediate allocation improvement. The rate-limit latency result does not: its reverse order moved only 2.43%-3.24%. The retained rate-limit claim is therefore limited to the deterministic removal of the framework-owned temporary String; no stable request-latency, throughput, RSS, or p99 percentage is claimed.
Module startup previously ran provider callbacks in module-name order, even when an importer sorted before its imported dependency. A reproducing test failed with MissingProvider before the fix. ModuleGraph now derives a dependency-first order during its existing cycle-detection traversal, owns the shared provider registration/initialization seam used by both core and facade bootstrap, and retains the documented name order of modules(). Synchronous registrars and sequential async initializers both run imported modules before importers; initializer failures still stop bootstrap with the original error. Transitive, diamond, independent, core-bootstrap, and facade-builder tests cover the order. This is reliability hardening, not a request-path optimization.
The 128-module validation control moved from 40.968-41.332 us at the detached baseline to 42.078-42.424 us for the final implementation. The point estimate increased from 41.142 us to 42.245 us (+2.68%, +1.103 us), inside the 5% noise threshold and confined to application startup. A second topological-map implementation measured 62.942-63.555 us and was rejected before finalizing the change.
The focused behavior commands were:
cargo test --locked -p nidus-config
cargo test --locked -p nidus-http --all-features
cargo test --locked -p nidus-core
cargo test --locked -p nidus-rs --all-features
The public rate-limit service future remains boxed. Replacing its nameable Tower associated type would be SemVer-sensitive, a private future enum is rejected by E0446, safe projection over arbitrary inner futures is not available without another public helper or a stronger Unpin bound, and the repository's prior concrete error-envelope future regressed. Default global concurrency limits, async-mutex swaps, eager singleton resolution, unsafe pin projection, and broad Cow migrations also remain rejected without workload-specific evidence.
Feature-isolated compile-surface pass (2026-07-26)
The workspace SQLx declaration previously enabled SQLite for every member that inherited it. As a result, an isolated PostgreSQL-only nidus-sqlx build still selected sqlx-sqlite and libsqlite3-sys. SQLite selection now belongs to the public sqlite feature of each owning crate; nidus-dashboard explicitly forwards that feature to SQLx. The PostgreSQL-only normal dependency graph drops from 149 to 142 unique packages, and sqlx-sqlite is absent. Explicit SQLite and combined-backend builds retain it.
The facade also declared tower-http unconditionally even though its only direct use is behind observability. The dependency is now optional and owned by that feature. A core-only nidus-rs --no-default-features graph drops from 53 to 33 unique packages and no longer includes tower-http, async-compression, compression-codecs, or compression-core. Observability builds retain tower-http. Minimal facade imports are now gated with the same features, so this configuration also passes with warnings denied.
The package counts use normal dependencies and deduplicate Cargo's repeated tree entries:
cargo tree --locked -p nidus-rs --no-default-features \
-e normal --prefix none --format '{p}' |
sed 's/ (\*)$//' | sort -u | wc -l
cargo tree --locked -p nidus-sqlx --no-default-features --features postgres \
-e normal --prefix none --format '{p}' |
sed 's/ (\*)$//' | sort -u | wc -l
bash scripts/check-integration-feature-matrix.sh
The feature-matrix script checks the facade's default, minimal, HTTP, observability, and all-feature configurations with warnings denied. It also checks every SQLx adapter backend independently, asserts that non-SQLite graphs exclude sqlx-sqlite, and asserts that explicit SQLite configurations for both adapters and the dashboard include it.
Clean cargo check wall times were compared with separate target directories against a detached worktree at the exact pre-change commit a8f2476, once in each execution order:
| Execution order | Target | Baseline | Candidate |
|---|---|---|---|
| Candidate, then baseline | Core-only facade | 5.80 s | 7.38 s |
| Candidate, then baseline | PostgreSQL-only SQLx adapter | 8.77 s | 10.42 s |
| Baseline, then candidate | Core-only facade | 7.91 s | 6.95 s |
| Baseline, then candidate | PostgreSQL-only SQLx adapter | 9.14 s | 10.21 s |
The timing does not establish a compile-time improvement: the facade result changed direction with execution order, and the SQLx candidate was slower in both samples. The retained claim is therefore limited to the deterministic dependency and compile surface reduction. No runtime latency, throughput, binary-size, or compile-time percentage is inferred from package removal.
As a separate code-quality cleanup, controller and injectable macro expansion now share one private parser for Inject<T> and Optional<T> fields instead of maintaining duplicate implementations. Macro unit tests and the complete trybuild pass/fail UI suite prove that accepted expansions and diagnostics remain unchanged. Facade integration targets now declare the features their fixtures actually require, and OpenAPI-only fixtures are compiled only with OpenAPI, so warnings-denied minimal and observability-only test runs are clean. These test-harness and macro changes are not presented as runtime optimizations.
Observability exposition and health response allocation pass (2026-07-26)
The observability Prometheus renderer previously allocated a formatted String for nearly every output line, rebuilt a Vec<String> plus joined label string for every histogram bucket/count/sum line, and performed three chained String::replace passes for every escaped label. It now writes directly into the final output with std::fmt::Write, streams label escaping, uses fixed-size label arrays, and reuses one rendered-label scratch buffer per histogram family. A conservative capacity estimate also reduces final output-buffer growth. Public methods, metric ordering, names, escaping, bucket boundaries, and numeric formatting are unchanged.
The retained exact-output test covers backslash, quote, and newline escaping, all finite duration buckets, +Inf, count, sum, and the three-label adapter shape. Existing event, job, lifecycle, adapter, cardinality, disabled-surface, HTTP-composition, and concurrent-render tests also remain green. The new Criterion group measures exposition with 1, 10, and 100 admitted lifecycle series.
The untouched baseline was saved with:
CARGO_TARGET_DIR=/tmp/nidus-observability-render-20260726 cargo bench --locked --bench integration_hot_paths -- 'observability prometheus render' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline before-observability-render-20260726
The candidate was compared twice with the identical harness and --baseline before-observability-render-20260726:
| Series | Baseline | Candidate A | Change A | Candidate B | Change B |
|---|---|---|---|---|---|
| 1 | 7.8491-7.9036 us | 1.3748-1.3833 us | -82.596% to -82.426% | 1.3544-1.3644 us | -82.958% to -82.787% |
| 10 | 69.329-69.774 us | 9.2984-9.3600 us | -86.637% to -86.508% | 9.4027-9.5405 us | -86.444% to -86.280% |
| 100 | 688.28-692.29 us | 90.551-91.178 us | -87.002% to -86.868% | 91.616-92.405 us | -86.804% to -86.640% |
All six comparisons reported p = 0.00. These are focused in-process scrape results, not an end-to-end server throughput or tail-latency claim.
A post-review confirmation run against the same untouched saved baseline measured 1.3126-1.3194 us for one series, 9.0429-9.1012 us for ten series, and 88.738-89.296 us for 100 series. The corresponding improvements were 83.290%-83.443%, 86.933%-87.046%, and 87.156%-87.275%; all three comparisons reported p = 0.00.
The health registry also cloned every registered check's Arc before polling and cloned every check name into the response map. The route future now borrows both values from its owned check slice until Axum serializes the JSON response. The synchronous check factory invocation remains inside the unwind-catching future, so panic, timeout, cancellation, ordering, detail-hiding, and status semantics are preserved.
The eight-check readiness row used:
CARGO_TARGET_DIR=/tmp/nidus-health-response-20260726 cargo bench --locked --bench request_lifecycle -- 'nidus health readiness with 8 checks' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline before-health-response-borrow-20260726
CARGO_TARGET_DIR=/tmp/nidus-health-response-20260726 cargo bench --locked --bench request_lifecycle -- 'nidus health readiness with 8 checks' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --baseline before-health-response-borrow-20260726
The untouched baseline was 1.6975-1.7198 us. Candidate runs measured 1.4897-1.4952 us (-13.606% to -12.325%, p = 0.00), 1.4398-1.4578 us, and 1.3956-1.4114 us (-19.122% to -17.423%, p = 0.00). The middle run's median remained 14.45% lower, but 13.33% high outliers widened Criterion's mean change interval to -8.0281% to -0.0284%; Criterion classified that comparison as within the configured 5% noise threshold. The source-level allocation removal and behavior tests are deterministic, but the exact latency improvement is therefore qualified rather than presented as a stable percentage.
A post-review confirmation run measured 1.4254-1.4361 us, a 15.839%-17.218% improvement over the same saved untouched baseline (p = 0.00).
Edition 2024 resolver alignment (2026-07-26)
The virtual workspace explicitly selected Cargo resolver 2 despite using Rust edition 2024 and MSRV 1.96. It now selects resolver 3, enabling Cargo's Rust-version-aware fallback for future dependency selection. This is dependency policy and reliability hardening, not a compile-time or runtime performance claim.
The committed lockfile did not change. Before and after the manifest edit, cargo metadata --locked --format-version 1 | shasum -a 256 produced c5e0b1cbf3e842d7489d38a06fdad963cd101c8b9984730976d9fe05501cd9b9, and cargo tree --locked --workspace -e features | shasum -a 256 produced f24b5449ea71791200b2952bd5945bafe9ac6a4970bbd76b6071d615410072eb. The full locked all-features test, Clippy, rustdoc, cargo-deny, and RustSec audit gates passed under resolver 3.
Compiler-enforced safe-only framework roots (2026-07-26)
All 26 framework, workspace-harness, and cargo-nidus crate roots now use #![forbid(unsafe_code)]. A source audit found no existing unsafe Rust syntax; the stronger forbid level prevents nested modules from locally weakening the policy. This adds no runtime code and makes the framework's existing safe-only implementation a compile-time invariant. Example binaries remain consumers of the framework rather than part of this crate-root policy.
The policy was checked with the locked all-features workspace test suite, warnings-denied Clippy, warnings-denied rustdoc, and the repository's dependency audit script. This is reliability hardening, not a performance claim.
Bounded Event Eviction Lock Scope (2026-07-22)
When a bounded event subscriber reaches capacity, publication now removes the oldest value, installs the new value, and releases the subscriber queue mutex before destroying the evicted value. Zero-capacity subscribers follow the same rule: the rejected value is returned from the guarded queue operation and destroyed only after the guard is released.
Previously, discarding the value returned by VecDeque::pop_front could run an arbitrary T::drop implementation while the subscriber queue remained locked. A slow destructor could unnecessarily extend the critical section, a reentrant destructor could deadlock while publishing or draining through the same queue, and a panicking destructor could poison the queue mutex. Queue mutation and payload destruction are now separate phases.
The change preserves public signatures, FIFO order, drop-oldest capacity behavior, zero-capacity behavior, subscriber registration order, event clone counts, partial-delivery boundaries, observer ordering, and the owned drain() -> Vec<T> contract. Unbounded publication and bounded publication that does not evict an event retain their existing behavior.
Focused regression coverage exercises reentrant destruction at capacities one and zero, blocked and panicking destructors, queue poison recovery, exact FIFO eviction, clone-panic partial delivery, concurrent publishers, and generated publish/drain schedules. The change is correctness and tail-latency isolation hardening rather than a general publication-throughput optimization.
Borrowed configuration and lifecycle rollback pass (2026-07-20)
Config previously cloned a complete serde_json::Value before every typed top-level, nested, and document deserialization. The typed APIs still require DeserializeOwned, but now deserialize directly from the stored value through serde_json's borrowed-value deserializer. Required nested lookup also resolves the path once instead of rebuilding the same owned path inside get_path_typed. Public method signatures, owned result types, and error variants are unchanged; focused coverage proves that repeated typed reads do not consume or mutate the stored document.
LifecycleRunner::startup previously allocated and grew a vector containing every successful hook index solely for failure rollback. Startup is strictly sequential, so a failure at index n means exactly 0..n completed. Rollback now traverses that range in reverse without bookkeeping allocation. Expanded coverage uses two successful hooks followed by a failing hook and asserts the same reverse-order shutdown sequence.
Both benchmark rows were added before either runtime implementation changed. Final evidence paired the current source with a detached worktree at the exact pre-change commit 198ac74, using separate target directories and running the pair in both execution orders. Only the identical benchmark harness was copied into the baseline worktree. Every run used 150 samples, a two-second warm-up, and a five-second measurement window:
git worktree add --detach /tmp/nidus-baseline-paired-20260720 198ac74
git diff 198ac74 -- Cargo.toml benches/configuration.rs benches/dependency_resolution.rs | git -C /tmp/nidus-baseline-paired-20260720 apply
CARGO_TARGET_DIR=/tmp/nidus-paired-baseline-target-20260720 cargo bench --bench dependency_resolution --bench configuration --no-run
# Pair 1: current source, then baseline 198ac74
CARGO_TARGET_DIR=/tmp/nidus-quality-20260720-target cargo bench --bench configuration -- 'nidus config deserialize 128 services' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline paired-current-first-20260720
CARGO_TARGET_DIR=/tmp/nidus-quality-20260720-target cargo bench --bench dependency_resolution -- 'nidus lifecycle startup with 32 hooks' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline paired-current-first-20260720
rsync -a /tmp/nidus-quality-20260720-target/criterion/ /tmp/nidus-paired-baseline-target-20260720/criterion/
CARGO_TARGET_DIR=/tmp/nidus-paired-baseline-target-20260720 cargo bench --bench configuration -- 'nidus config deserialize 128 services' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --baseline paired-current-first-20260720
CARGO_TARGET_DIR=/tmp/nidus-paired-baseline-target-20260720 cargo bench --bench dependency_resolution -- 'nidus lifecycle startup with 32 hooks' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --baseline paired-current-first-20260720
# Pair 2: baseline 198ac74, then current source
CARGO_TARGET_DIR=/tmp/nidus-paired-baseline-target-20260720 cargo bench --bench configuration -- 'nidus config deserialize 128 services' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline paired-baseline-first-20260720
CARGO_TARGET_DIR=/tmp/nidus-paired-baseline-target-20260720 cargo bench --bench dependency_resolution -- 'nidus lifecycle startup with 32 hooks' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline paired-baseline-first-20260720
rsync -a /tmp/nidus-paired-baseline-target-20260720/criterion/ /tmp/nidus-quality-20260720-target/criterion/
CARGO_TARGET_DIR=/tmp/nidus-quality-20260720-target cargo bench --bench configuration -- 'nidus config deserialize 128 services' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --baseline paired-baseline-first-20260720
CARGO_TARGET_DIR=/tmp/nidus-quality-20260720-target cargo bench --bench dependency_resolution -- 'nidus lifecycle startup with 32 hooks' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --baseline paired-baseline-first-20260720
| Execution order | Benchmark | Baseline 198ac74 | Current source | Current-source change |
|---|---|---|---|---|
| Current, then baseline | Deserialize 128 services | 24.437-24.595 us | 3.2267-3.2387 us | 86.64%-86.74% faster |
| Baseline, then current | Deserialize 128 services | 24.225-24.305 us | 3.3784-3.4801 us | 86.04%-86.38% faster |
| Current, then baseline | Start 32 lifecycle hooks | 725.31-732.57 ns | 575.49-581.08 ns | 20.23%-20.94% faster |
| Baseline, then current | Start 32 lifecycle hooks | 767.84-793.84 ns | 613.58-628.86 ns | 18.48%-21.65% faster |
Criterion classified all four comparisons as improvements (p = 0.00). These are isolated in-memory configuration and lifecycle microbenchmarks. They do not establish request latency, server throughput, or startup performance for hooks that perform substantial application work.
Logging redaction lookup pass (2026-07-17)
LoggingConfig::redacts_header previously allocated an ASCII-lowercase String for every lookup before searching its redaction set. The configuration now normalizes only when a policy is added, keeps the private names in sorted order, and compares ASCII-normalized byte iterators with binary search. Lookup remains case-insensitive and logarithmic in the number of configured names, but no longer allocates. Construction still deduplicates normalized names; focused tests cover insertion order, duplicates, lowercase and mixed-case hits, misses, and the existing ASCII-only normalization behavior.
The two benchmark rows were added before the implementation and measured in one isolated target directory. Both sides used 200 samples, a two-second warm-up, and a five-second measurement window. The implementation comparison was then repeated without changing the source:
CARGO_TARGET_DIR=/tmp/nidus-logging-redaction-20260717 cargo bench --bench request_lifecycle -- 'nidus logging redaction (lowercase|mixed-case) lookup' --warm-up-time 2 --measurement-time 5 --sample-size 200 --noplot --save-baseline before-logging-redaction-20260717
CARGO_TARGET_DIR=/tmp/nidus-logging-redaction-20260717 cargo bench --bench request_lifecycle -- 'nidus logging redaction (lowercase|mixed-case) lookup' --warm-up-time 2 --measurement-time 5 --sample-size 200 --noplot --baseline before-logging-redaction-20260717
CARGO_TARGET_DIR=/tmp/nidus-logging-redaction-20260717 cargo bench --bench request_lifecycle -- 'nidus logging redaction (lowercase|mixed-case) lookup' --warm-up-time 2 --measurement-time 5 --sample-size 200 --noplot --baseline before-logging-redaction-20260717
| Benchmark | Before | First implementation run | Repeated implementation run |
|---|---|---|---|
| Lowercase hit | 18.861-19.022 ns | 10.427-11.045 ns (43.52%-45.76% faster) | 11.433-11.960 ns (39.09%-41.73% faster) |
| Mixed-case hit | 19.790-20.122 ns | 9.8489-10.231 ns (48.76%-50.29% faster) | 10.213-10.619 ns (42.82%-45.79% faster) |
Criterion classified all four comparisons as improvements (p = 0.00). These are isolated lookup microbenchmarks, not request-latency or application- throughput claims. An earlier exact-match fast path improved lowercase input but regressed mixed-case input by 5.00%-6.81%, so it was rejected. A separate request-scope extractor experiment improved its first comparison by 2.36%-5.17% but was statistically unchanged when repeated (p = 0.81); it too was reverted.
Module validation small-collection pass (2026-07-17)
Module validation previously constructed BTreeSet indexes for every non-empty imports, providers, controllers, and exports list, plus additional provider membership indexes even when a module had only one provider or no relevant controllers/imports. The common feature-module shape therefore paid several small heap allocations to validate metadata whose uniqueness or membership is trivial. Validation now skips uniqueness indexes for zero or one name and uses an allocation-free empty/single-name lookup while retaining BTreeSet for larger collections. Validation pass order, deterministic graph order, error variants, and error payload order are unchanged.
The existing Criterion rows were present before the implementation. The 128-module benchmark constructs 128 feature modules with one provider/export each plus a root importing all features in untimed iter_batched setup. The first-singleton row likewise constructs and registers the container during untimed setup and serves as a negative control because this pass does not change provider resolution.
The initially saved same-target baseline became invalid after the host's timing shifted enough to move the unchanged control by double digits. Final evidence therefore used a detached baseline worktree at e745f70, separately compiled target directories, adjacent measurements in both execution orders, and copied only Criterion's saved statistics between targets. Every run used 150 samples, a two-second warm-up, and a five-second measurement window:
git worktree add --detach /tmp/nidus-baseline-paired-20260717 e745f70
CARGO_TARGET_DIR=/tmp/nidus-core-paired-baseline-target-20260717 cargo bench --bench dependency_resolution --no-run # baseline e745f70
CARGO_TARGET_DIR=/tmp/nidus-core-paired-current-target-20260717 cargo bench --bench dependency_resolution --no-run # current source
# Pair 1: current source, then baseline e745f70
CARGO_TARGET_DIR=/tmp/nidus-core-paired-current-target-20260717 cargo bench --bench dependency_resolution -- 'nidus (singleton first resolution|128-module graph validation)' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline paired-correct-current-1
rsync -a /tmp/nidus-core-paired-current-target-20260717/criterion/ /tmp/nidus-core-paired-baseline-target-20260717/criterion/
CARGO_TARGET_DIR=/tmp/nidus-core-paired-baseline-target-20260717 cargo bench --bench dependency_resolution -- 'nidus (singleton first resolution|128-module graph validation)' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --baseline paired-correct-current-1
# Pair 2: baseline e745f70, then current source
CARGO_TARGET_DIR=/tmp/nidus-core-paired-baseline-target-20260717 cargo bench --bench dependency_resolution -- 'nidus (singleton first resolution|128-module graph validation)' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline paired-correct-old-2
rsync -a /tmp/nidus-core-paired-baseline-target-20260717/criterion/ /tmp/nidus-core-paired-current-target-20260717/criterion/
CARGO_TARGET_DIR=/tmp/nidus-core-paired-current-target-20260717 cargo bench --bench dependency_resolution -- 'nidus (singleton first resolution|128-module graph validation)' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --baseline paired-correct-old-2
| Order | Baseline e745f70 | Current source | Current-source change |
|---|---|---|---|
| Current, then baseline | 59.929-60.507 us | 39.789-40.160 us | 32.99%-34.24% faster |
| Baseline, then current | 59.268-59.562 us | 41.313-41.683 us | 29.57%-30.66% faster |
Criterion found a graph-validation difference in both orders (p = 0.00). The unchanged singleton control moved by 1.16%-4.99% in the order-correlated direction, materially less than the 29.57%-34.24% graph separation. An attempted resolution-stack fast path was restored to its original implementation after its broader benchmark result failed to reproduce; focused tests were retained for normal nested and defensive out-of-order teardown. The graph result is an isolated validation microbenchmark and does not establish request latency, application startup time, or throughput.
Event fan-out allocation pass (2026-07-17)
EventBus::publish previously collected every live subscriber into a fresh Vec, including the common one-subscriber case, and cloned the event once for every target. Publication now keeps the first live queue inline, allocates an additional-target vector only when fan-out requires it, clones for all but the final target, and moves the original event into that final queue. The related subscriber_count path now prunes weak handles in place instead of building a temporary vector of strong handles just to obtain its length.
The public event, subscriber, queue-capacity, ordering, and overflow-policy APIs are unchanged. A clone-count regression proves that publication performs zero event clones for one subscriber and one clone for two subscribers, while the existing delivery assertions cover both queues. The existing Criterion one-subscriber and four-subscriber controls were measured with 150 samples, a two-second warm-up, and a five-second measurement window:
cargo bench --bench event_bus -- 'nidus (single-subscriber bounded event publish|four-subscriber bounded event publish)' --warm-up-time 2 --measurement-time 5 --sample-size 150 --save-baseline pre_event_fanout_20260717
cargo bench --bench event_bus -- 'nidus (single-subscriber bounded event publish|four-subscriber bounded event publish)' --warm-up-time 2 --measurement-time 5 --sample-size 150 --baseline pre_event_fanout_20260717
cargo bench --bench event_bus -- 'nidus four-subscriber bounded event publish' --warm-up-time 2 --measurement-time 5 --sample-size 150 --baseline pre_event_fanout_20260717
| Benchmark | Before | Current source | Criterion change |
|---|---|---|---|
| One-subscriber bounded publish | 29.852-30.628 ns | 11.682-11.832 ns | 59.71%-60.80% faster (p = 0.00) |
| Four-subscriber bounded publish | 47.332-48.761 ns | 45.774-46.444 ns | no statistically significant change (p = 0.75) |
This is an isolated in-process publication microbenchmark. It does not establish application throughput or contended multi-producer behavior, and the four-subscriber result is deliberately reported as unchanged rather than as an improvement.
Singleton state ownership pass (2026-07-17)
The singleton state machine previously kept the resolved Arc both in its lock-protected Ready variant and in the authoritative OnceLock fast-path cache. Every initialized singleton therefore retained an unnecessary strong reference, and the payload made every ProviderEntry larger, including entries whose lifetime never uses singleton storage. Ready is now a unit state; waiters read the already-published value from OnceLock after observing that state.
The public container, provider, and lifetime APIs are unchanged. The existing retry, poisoned-lock, panic-recovery, circular-resolution, and concurrent singleton tests remain in place. A focused ownership assertion proves that the cache plus two callers are the only strong references held by a directly constructed provider entry. On this aarch64-apple-darwin build, size_of::<ProviderEntry>() moved from 152 to 128 bytes, and the first returned registered singleton had three strong references instead of four. Those layout figures are build observations, not a stable ABI guarantee.
The new first-resolution row uses iter_batched: container creation and registration happen in untimed setup, while the measured routine performs the first singleton resolution. The benchmark definition was present on both sides of the comparison. All runs used one isolated target directory, 150 samples, a two-second warm-up, and a five-second measurement window:
CARGO_TARGET_DIR=/tmp/nidus-singleton-pass-20260717 cargo bench --bench dependency_resolution -- 'nidus singleton first resolution' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline before-singleton-state-20260717
CARGO_TARGET_DIR=/tmp/nidus-singleton-pass-20260717 cargo bench --bench dependency_resolution -- 'nidus singleton first resolution' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --baseline before-singleton-state-20260717
| Benchmark | Before | First current-source run | Repeated current-source run |
|---|---|---|---|
| First singleton resolution | 125.10-129.98 ns | 106.71-112.09 ns (11.59%-16.19% faster) | 118.18-124.99 ns (5.57%-10.59% faster) |
Criterion classified both comparisons as improvements (p = 0.00). This is an isolated first-resolution microbenchmark, not an application-startup, steady-state request-latency, or throughput claim. The existing lock-free steady-state resolution path is intentionally unchanged.
Module-graph allocation pass (2026-07-16)
Successful ModuleGraph::from_modules validation previously cloned every module name twice while building the graph index, copied graph-owned names into the cycle-detection sets and stack, and allocated a Vec for every visible provider even when only one import exported it. The implementation now uses the BTreeMap entry API to move the index key once, borrows graph-owned names during depth-first traversal, and promotes a visible provider from one borrowed import to an owned ambiguity list only when a second exporter appears.
The public graph, builder, and error APIs are unchanged. Focused tests assert the exact duplicate-module name, ordered cycle path, and ordered ambiguous import list so the success-path ownership changes cannot weaken diagnostics.
The consuming benchmark uses Criterion iter_batched: constructing 128 feature modules plus the root definition happens in the untimed setup routine, while the measured routine owns and validates those definitions. Both source versions used the same fresh target directory, 150 samples, a two-second warm-up, and a five-second measurement window. The current source was measured twice against the saved pre-change baseline:
CARGO_TARGET_DIR=/tmp/nidus-module-graph-20260716 cargo bench --bench dependency_resolution -- 'nidus 128-module graph validation' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline before-module-graph-20260716
CARGO_TARGET_DIR=/tmp/nidus-module-graph-20260716 cargo bench --bench dependency_resolution -- 'nidus 128-module graph validation' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --baseline before-module-graph-20260716
| Benchmark | Before | First current-source run | Repeated current-source run |
|---|---|---|---|
| 128 feature imports plus root validation | 93.387-95.887 us | 66.904-68.444 us (25.42%-28.70% faster) | 67.110-69.084 us (27.48%-30.54% faster) |
Criterion classified both comparisons as improvements (p = 0.00). This is an isolated in-process startup validation benchmark, not a request-latency, throughput, or application-startup claim.
Health-check task-elision pass (2026-07-16)
HealthRoute::run_checks previously created one Tokio task and JoinHandle for every registered check, then retained a custom drop guard to abort those tasks when the request was cancelled. That added task scheduling, handle allocation, and cancellation bookkeeping to a route whose checks are already async. The implementation now polls the checks concurrently with futures-util::join_all, catches both synchronous and asynchronous panics, and lets cancellation drop the in-process futures directly. Check order, per-check timeouts, panic-to-down responses, response ordering, and cancellation are covered by crates/nidus-http/tests/production_api.rs.
The focused request-lifecycle benchmark was run from one isolated target directory. The first command saved the pre-change source baseline; the second ran the current source against that baseline:
CARGO_TARGET_DIR=/tmp/nidus-health-before cargo bench --bench request_lifecycle --all-features -- 'nidus health readiness with 8 checks' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --save-baseline before-health
CARGO_TARGET_DIR=/tmp/nidus-health-before cargo bench --bench request_lifecycle --all-features -- 'nidus health readiness with 8 checks' --warm-up-time 2 --measurement-time 5 --sample-size 150 --noplot --baseline before-health
| Benchmark | Before | After | Criterion change |
|---|---|---|---|
| Health readiness with 8 checks | 11.461-12.290 us | 1.5258-1.5482 us | 88.144%-89.760% faster |
Criterion classified the comparison as an improvement (p = 0.00). This is an isolated in-process route-latency measurement, not a server-throughput claim. The change deliberately does not alter the public health API. Blocking synchronous checks remain an application responsibility and should use spawn_blocking or an async client.
Middleware configuration borrowing pass (2026-07-16)
RateLimitService::call previously cloned a complete RateLimitConfig before performing synchronous identity and store checks. That cloned both the store and identity Arcs, then dropped them before returning the response future. ValidatedRequestIdService::call likewise cloned its complete config, retaining an extra generator Arc and header-name clone across the inner future. Both services now borrow their config during synchronous work; request-ID middleware moves only the response header name across the future boundary. Public config, layer, service, error-policy, and header behavior are unchanged.
Two unit tests make the ownership change deterministic: the rate-limit store observes exactly one strong owner during its check, and the request-ID generator has no additional owner across the inner response future. Separately compiled release benchmark artifacts showed smaller Service::call symbol spans for the measured monomorphs: 1,284 to 988 bytes for rate limiting (296 bytes) and 2,844 to 2,688 bytes for validated request IDs (156 bytes). These are observations from these artifacts, not universal code-size or latency guarantees.
The relevant Criterion rows were also run from independently compiled baseline and current targets, alternating the order:
CARGO_TARGET_DIR=/tmp/nidus-pre-config-borrow-target cargo bench --bench request_lifecycle --all-features -- 'nidus middleware (validated request id|rate limit) request' --warm-up-time 3 --measurement-time 6 --sample-size 150 --noplot
CARGO_TARGET_DIR=/tmp/nidus-quality-20260716-target cargo bench --bench request_lifecycle --all-features -- 'nidus middleware (validated request id|rate limit) request' --warm-up-time 3 --measurement-time 6 --sample-size 150 --noplot
The first baseline/current pair moved both rows roughly 7% slower in the current run, while the reverse pair moved validated request IDs faster and rate limiting slower. Concurrent system compilation and macOS executable scanning were active, so the comparisons did not meet the repository's evidence bar. No end-to-end latency percentage is recorded for this pass; the accepted evidence is the ownership tests plus the observed release-artifact symbol reduction.
Moka cache key allocation pass (2026-07-14)
MokaCacheProvider::get and invalidate previously constructed an owned CacheKey for every operation, even when no namespace was configured and Moka could look up the existing String key through a borrowed &str. The unnamespaced paths now pass that borrowed key directly. Namespaced keys still own one string, but compose it into one exactly sized buffer instead of using formatting machinery. Inserts remain owned because Moka must retain their keys.
The benchmark definition was added before the implementation change. Both sides ran in the same dedicated target directory on the same aarch64-apple-darwin machine with rustc 1.96.0, using 150 samples, a two-second warm-up, and a five-second measurement window. Empty cached values keep value cloning out of the measured key-path comparison:
CARGO_TARGET_DIR=target/cache-key-pass cargo bench -p nidus-cache --bench cache_hot_paths -- --save-baseline before-cache-key-pass-20260714 --noplot --warm-up-time 2 --measurement-time 5 --sample-size 150
CARGO_TARGET_DIR=target/cache-key-pass cargo bench -p nidus-cache --bench cache_hot_paths -- --baseline before-cache-key-pass-20260714 --noplot --warm-up-time 2 --measurement-time 5 --sample-size 150
| Benchmark | Before | First final-source run | Repeated final-source run |
|---|---|---|---|
| Get without namespace | 172.49-175.69 ns | 139.48-143.27 ns (23.71%-33.17% faster) | 137.10-140.61 ns (26.96%-35.99% faster) |
| Get with namespace | 180.27-182.59 ns | 148.77-156.04 ns (18.09%-20.22% faster) | 145.21-146.15 ns (20.15%-21.44% faster) |
Criterion classified all four final comparisons as improvements (p = 0.00). These are isolated in-process cache-hit measurements, not end-to-end request or service-throughput claims. An initial Cow<str> implementation improved the unnamespaced row but regressed the namespaced control by 11.53%-17.21%; it was rejected in favor of the explicit branches and pre-sized owned-key path above.
Observability label interning pass (2026-07-14)
The non-HTTP observability collector previously allocated fresh String keys for every repeated event, job, and lifecycle observation. Adapter recording also formatted the adapter and operation into one temporary string, split it again, and allocated the resulting fields and status for both the counter and histogram maps.
Stable dynamic labels are now interned as Arc<str> values and repeated records clone only the shared reference. Static status values remain borrowed, and adapter identity is a private typed pair of static strings. Besides removing the recording-path allocations, the typed pair prevents two distinct adapter labels containing : from being merged or rendered with the wrong boundary. The public API, metric names, normal output ordering, and bounded-cardinality overflow policy are unchanged.
The benchmark definitions were applied before the implementation change, then measured on both sides in the same dedicated target directory. Both sides ran on the same aarch64-apple-darwin machine with rustc 1.96.0, using 100 samples, a three-second warm-up, and a five-second measurement window:
CARGO_TARGET_DIR=target/observability-label-pass cargo bench --bench integration_hot_paths --all-features -- observability --save-baseline before-observability-labels --noplot --warm-up-time 3 --measurement-time 5 --sample-size 100
CARGO_TARGET_DIR=target/observability-label-pass cargo bench --bench integration_hot_paths --all-features -- observability --baseline before-observability-labels --noplot --warm-up-time 3 --measurement-time 5 --sample-size 100
| Benchmark | Before | Confirming implementation run | Criterion change |
|---|---|---|---|
| Lifecycle record | 88.857-89.604 ns | 25.309-25.867 ns | 71.43%-72.04% faster |
| Adapter record | 197.55-200.70 ns | 41.503-42.046 ns | 78.79%-79.23% faster |
| Event record | 36.765-37.450 ns | 23.411-23.690 ns | 34.74%-36.27% faster |
Criterion classified all three changes as improvements (p = 0.00). Two earlier implementation comparisons reported larger improvements; the table uses the slower final-source run. These are repeated-label, in-process collector microbenchmarks, not end-to-end service throughput claims.
Broader candidates were deliberately rejected for this pass. Allocator swaps, blanket inlining, and release-profile tuning are workload or deployment dependent and lack an isolated repository proof. Changing provider lifetimes, eager singleton behavior, or HTTP lifecycle coupling would change semantics. Previously measured boxed-future and event fan-out experiments were not retried without new evidence after their control workloads regressed.
Trusted-proxy identity pass (2026-07-14)
trusted_proxy_client_ip_identity previously copied the complete X-Forwarded-For header before parsing one address and captured the configured proxy list in a Vec, so cloning the extractor also cloned that allocation. Forwarded values are now parsed in place, while the immutable proxy list is shared as Arc<[IpAddr]>.
The trust algorithm now starts at Axum's connected peer and walks all forwarded header values from right to left only while each hop is trusted. The first non-trusted address is the client identity; malformed hops stop traversal at the last verified address. Focused tests cover trusted multi-proxy chains, attacker-controlled prefixes, split header fields, malformed values, and untrusted direct peers.
The same aarch64-apple-darwin checkout and rustc 1.96.0 used 150 samples, a two-second warm-up, and a five-second measurement window. The extraction row uses one trusted direct proxy and one forwarded client; the clone row uses an eight-proxy configuration:
cargo bench --bench request_lifecycle --all-features -- 'nidus trusted proxy' --warm-up-time 2 --measurement-time 5 --sample-size 150 --save-baseline pre_trusted_proxy_chain_20260714
cargo bench --bench request_lifecycle --all-features -- 'nidus trusted proxy' --warm-up-time 2 --measurement-time 5 --sample-size 150 --baseline pre_trusted_proxy_chain_20260714
| Benchmark | Before | First implementation run | Repeated implementation run |
|---|---|---|---|
| Client-IP extraction | 112.47-114.32 ns | 99.445-100.75 ns (8.88%-11.18% faster) | 98.781-100.97 ns (11.19%-12.64% faster) |
| Eight-proxy extractor clone | 15.533-16.226 ns | 3.3060-3.3782 ns (79.22%-79.95% faster) | 3.2071-3.2477 ns (79.57%-80.25% faster) |
Criterion classified all four comparisons as improvements (p = 0.00). These are isolated in-process identity microbenchmarks, not end-to-end request or server-throughput claims.
OpenAPI method allocation pass (2026-07-14)
OpenApiRoute previously stored its fixed HTTP method as an owned String, so each manual get, post, put, patch, or delete route paid for a heap allocation. The private field now uses Cow<'static, str> and borrows those five lowercase literals. Generated RouteMetadata uses the same borrowed literals for the framework-supported uppercase/lowercase methods and retains an owned lowercase fallback for uncommon methods.
The same aarch64-apple-darwin checkout and rustc 1.96.0 used 100 samples, a two-second warm-up, and a five-second measurement window:
cargo bench --bench request_lifecycle -- 'nidus 100-route openapi document construction' --warm-up-time 2 --measurement-time 5 --sample-size 100 --save-baseline before-openapi-index-100-20260714
cargo bench --bench request_lifecycle -- 'nidus 100-route openapi document construction' --warm-up-time 2 --measurement-time 5 --sample-size 100 --baseline before-openapi-index-100-20260714
cargo bench --bench request_lifecycle -- 'nidus 8-route openapi document construction' --warm-up-time 2 --measurement-time 5 --sample-size 100 --save-baseline before-openapi-index-8-20260714
cargo bench --bench request_lifecycle -- 'nidus 8-route openapi document construction' --warm-up-time 2 --measurement-time 5 --sample-size 100 --baseline before-openapi-index-8-20260714
cargo bench --bench request_lifecycle -- 'nidus 100-route openapi metadata construction' --warm-up-time 2 --measurement-time 5 --sample-size 100 --save-baseline before-openapi-metadata-cow-20260714
cargo bench --bench request_lifecycle -- 'nidus 100-route openapi metadata construction' --warm-up-time 2 --measurement-time 5 --sample-size 100 --baseline before-openapi-metadata-cow-20260714
| Benchmark | Before | After | Criterion change |
|---|---|---|---|
| 8-route manual construction | 2.7585-2.8047 us | 2.6277-2.6494 us | 4.23%-5.72% faster |
| 100-route manual construction | 40.022-40.766 us | 37.607-38.027 us | 5.69%-7.87% faster |
| 100-route generated-metadata construction | 31.437-31.650 us | 31.245-31.552 us | no change detected |
Criterion classified both manual-route comparisons as improvements (p = 0.00). The generated-metadata control was statistically unchanged (p = 0.90, change interval -0.55% to +0.50%), so no generated-metadata performance claim is made. Tests cover all five manual builders, generated metadata, duplicate operations after many routes and cloning, and the uncommon-method lowercase fallback.
Several broader experiments were rejected. An event-bus single-subscriber inline collector improved that row by more than 58%, but its best fan-out-safe shape still regressed four subscribers by 3.87%-5.80%. A lazy OpenAPI hash index improved 100 routes but regressed 16 routes by 11.63%-13.90% and 32 routes by 9.90%-11.36%. Route/schema capacity reservations stayed within Criterion's noise threshold. All three experiments were reverted instead of being presented as general optimizations.
Request-context correlation fallback pass (2026-07-14)
RequestContext::from_parts previously cloned the final request-ID String when no explicit x-correlation-id header was present. That is the normal production path, and the clone existed only so correlation_id() could return the same bytes. The private context representation now records that the correlation ID refers to the existing request ID, while preserving the public constructor and getter behavior for explicit, fallback, and absent values.
The saved baseline was built from a detached worktree at the exact pre-change commit (6dad920). The edited checkout then used the same target directory and Criterion baseline. Both sides used 200 samples, a five-second warm-up, and a ten-second measurement window on the same aarch64-apple-darwin machine with rustc 1.96.0:
# Run from the detached 6dad920 worktree.
CARGO_TARGET_DIR="$NIDUS_TARGET" cargo bench --bench request_lifecycle --all-features -- --save-baseline context-clean-before-20260714 --warm-up-time 5 --measurement-time 10 --sample-size 200 'nidus (middleware request context|api defaults production) request'
# Run twice from the edited checkout.
CARGO_TARGET_DIR="$NIDUS_TARGET" cargo bench --bench request_lifecycle --all-features -- --baseline context-clean-before-20260714 --warm-up-time 5 --measurement-time 10 --sample-size 200 'nidus (middleware request context|api defaults production) request'
| Benchmark | Before | First implementation run | Repeated implementation run |
|---|---|---|---|
| Request-context middleware | 909.95-923.93 ns | 861.19-865.31 ns (6.35%-7.96% faster) | 871.02-879.19 ns (4.51%-6.28% faster) |
| Production defaults | 2.3833-2.4454 us | 2.2166-2.2481 us (6.96%-9.79% faster) | 2.2055-2.2262 us (7.83%-10.59% faster) |
Criterion classified all four comparisons as improvements (p = 0.00). These are local in-process request measurements, not end-to-end throughput claims. Focused tests also inspect the private fallback state, deterministically proving that it carries no second owned String.
An additional experiment replaced the error-envelope service's boxed future with a concrete futures-util composition. A clean detached-worktree A/B moved the successful-response row from 671.67-676.20 ns to 719.08-733.51 ns, a 7.67%-9.62% regression (p = 0.00). That experiment was reverted; the public Tower service future and the measured implementation remain unchanged.
Guard route-label sharing pass (2026-07-14)
GuardLayer, GuardService, and GuardContext previously retained route labels as owned String values. Router/service cloning and every guarded request therefore cloned an immutable label. They now retain one Arc<str> and clone only its reference count. Macro-generated container-composed guards also retain one shared route label per route and move the final synthetic header map into the last guard context instead of cloning it again.
The existing explicit guard row was measured immediately before and after the change on the same aarch64-apple-darwin machine with rustc 1.96.0. Both sides used 150 samples, a two-second warm-up, and a five-second measurement window:
cargo bench --bench request_lifecycle -- 'nidus guarded route' --warm-up-time 2 --measurement-time 5 --sample-size 150 --save-baseline pre_guard_label_arc_20260714
cargo bench --bench request_lifecycle -- 'nidus guarded route$' --warm-up-time 2 --measurement-time 5 --sample-size 150 --baseline pre_guard_label_arc_20260714
The saved baseline measured 886.93-927.27 ns. The immediate implementation run measured 687.71-710.61 ns, with Criterion reporting an 18.33%-22.87% improvement (p = 0.00). A later repeat measured 629.59-647.08 ns and was also classified as an improvement. These are local in-process route measurements, not end-to-end throughput claims.
A new nidus module-composed guarded route benchmark separately covers the generated container path:
cargo bench --bench request_lifecycle -- 'nidus module-composed guarded route' --warm-up-time 2 --measurement-time 5 --sample-size 150 --save-baseline pre_macro_guard_context_20260714
cargo bench --bench request_lifecycle -- 'nidus module-composed guarded route' --warm-up-time 2 --measurement-time 5 --sample-size 150 --baseline pre_macro_guard_context_20260714
Its repeated latency comparison moved only 0.45%-1.61% lower and remained within Criterion's noise threshold, so no latency improvement is claimed for that row. The source change deterministically removes the final header-map clone and per-request label allocation, while a two-guard runtime test proves that ordered checks still receive request headers.
HTTP success-path middleware pass (2026-07-13)
The production error envelope previously allocated owned path and request-ID strings before knowing whether the response was an error. It now retains the request metadata and creates those strings only for 4xx/5xx responses. The panic-catching layer previously boxed its response future for every request; it now composes the existing concrete futures-util future types while preserving both synchronous Service::call and asynchronous polling panic handling.
The benchmark definitions were identical on both sides. Both measurements ran on the same aarch64-apple-darwin machine with rustc 1.96.0, using 150 samples, a two-second warm-up, and a five-second measurement window:
CARGO_TARGET_DIR=target/quality-pass cargo bench --bench request_lifecycle -- 'nidus middleware (error envelope|catch panic) success request' --warm-up-time 2 --measurement-time 5 --sample-size 150 --save-baseline pre_http_success_paths
CARGO_TARGET_DIR=target/quality-pass cargo bench --bench request_lifecycle -- 'nidus middleware (error envelope|catch panic) success request' --warm-up-time 2 --measurement-time 5 --sample-size 150 --baseline pre_http_success_paths
| Benchmark | Before | After | Criterion change |
|---|---|---|---|
| Error envelope, successful response | 711.61-725.38 ns | 666.96-700.17 ns | 4.98%-7.21% faster |
| Panic catcher, non-panicking response | 598.33-607.41 ns | 554.40-559.28 ns | 6.66%-8.19% faster |
Criterion classified both changes as improvements (p = 0.00). These are isolated in-process middleware measurements, not end-to-end server throughput claims. Existing error-envelope behavior tests and both synchronous-call and future-poll panic regression tests remain green.
Prometheus label interning pass (2026-07-13)
The in-process collector previously converted both the HTTP method and route label into new String values on request start and completion. It now keeps http::Method as the map key and interns each admitted route as an Arc<str>, including one shared overflow label for capped collectors. The public API, rendered metric names and labels, and route-cardinality policy are unchanged.
The same three existing Criterion rows were measured before and after the change on the same aarch64-apple-darwin machine with rustc 1.96.0. Both sides used 150 samples, a two-second warm-up, and a five-second measurement window:
cargo bench --bench request_lifecycle -- 'nidus (metrics record response|metrics record inner error|api defaults production with metrics request)' --warm-up-time 2 --measurement-time 5 --sample-size 150 --save-baseline pre_quality_20260713
cargo bench --bench request_lifecycle -- 'nidus (metrics record response|metrics record inner error|api defaults production with metrics request)' --warm-up-time 2 --measurement-time 5 --sample-size 150 --baseline pre_quality_20260713
| Benchmark | Before | After | Criterion change |
|---|---|---|---|
| Production defaults with metrics | 3.4368-3.5288 us | 2.6430-2.6622 us | 22.44%-24.35% faster |
| Metrics record response | 117.25-121.13 ns | 48.162-48.397 ns | 60.37%-61.42% faster |
| Metrics record inner error | 117.54-122.23 ns | 48.725-48.935 ns | 56.66%-57.84% faster |
Criterion classified all three changes as improvements (p = 0.00). These are local request-lifecycle and collector microbenchmarks, not end-to-end server throughput claims.
OpenAPI path and operation-ID allocation pass (2026-07-12)
OpenAPI path normalization and operation-ID rendering were changed from a temporary Vec<String> plus per-segment strings to one pre-sized output String. A new 100-route construction/render row was applied identically before and after the implementation change. Both sides used 150 samples, a two-second warm-up, and a five-second measurement window on the same aarch64-apple-darwin machine with rustc 1.96.0:
cargo bench --bench request_lifecycle -- 'nidus 100-route openapi document render' --warm-up-time 2 --measurement-time 5 --sample-size 150 --save-baseline pre_elite_20260712
cargo bench --bench request_lifecycle -- 'nidus 100-route openapi document render' --warm-up-time 2 --measurement-time 5 --sample-size 150 --baseline pre_elite_20260712
The saved baseline measured 341.66-351.44 us. The confirming implementation run measured 280.76-283.64 us, with Criterion reporting an 18.40%-20.56% improvement (p = 0.00). This is a local document-build microbenchmark, not an HTTP throughput claim.
First-party integration baseline (2026-07-11)
The new envelope and durable-job hot paths were captured and immediately repeated on the same aarch64-apple-darwin machine with rustc 1.96.0, 100 samples, a one-second warm-up, and a three-second measurement window:
cargo bench --bench integration_hot_paths -- --warm-up-time 1 --measurement-time 3 --sample-size 100 --save-baseline integration_initial
cargo bench --bench integration_hot_paths -- --warm-up-time 1 --measurement-time 3 --sample-size 100 --baseline integration_initial
| Benchmark | Initial interval | Confirming interval | Result |
|---|---|---|---|
| Envelope serialize, 1 KiB | 558.23-562.54 ns | 560.33-563.86 ns | no change |
| Envelope deserialize, 1 KiB | 534.84-550.46 ns | 525.53-527.74 ns | within threshold, lower |
| Durable job validate/construct, 1 KiB | 1.3829-1.3907 us | 1.3754-1.3985 us | within threshold |
| Retry bound calculation | 1.4657-1.4753 ns | 1.3555-1.3609 ns | improved |
An intervening identical-binary sample moved the 1.5 ns retry arithmetic row by 2.8%, demonstrating why this suite uses an explicit 5% noise threshold. The confirming run had no regressions beyond that threshold.
The final release-state run repeated the same command after all service and failure-path gates. A pre-final sample had reported a 6.47% regression in job construction, so it was not waived: payload validation was changed from serializing into a temporary Vec to an allocation-free bounded counting writer while preserving the exact 1 MiB serialized-size limit. A 150-sample isolated confirmation then reported [1.4033 us, 1.4258 us], a statistically insignificant +0.75% (p = 0.31). The complete final run reported:
| Benchmark | Final interval | Change from saved baseline | Result |
|---|---|---|---|
| Envelope serialize, 1 KiB | 575.80-580.08 ns | +2.15% | within threshold |
| Envelope deserialize, 1 KiB | 538.50-540.29 ns | +0.91% | within threshold |
| Durable job validate/construct, 1 KiB | 1.3464-1.3521 us | -4.17% | within threshold |
| Retry bound calculation | 1.4157-1.4317 ns | -2.81% | within threshold |
No final row exceeded the documented 5% release threshold.
1.0.9 routing and OpenAPI builder pass (2026-07-10)
Repeated path normalization and cumulative OpenAPI schema-map cloning were measured with saved Criterion baselines on the same aarch64-apple-darwin machine and rustc 1.96.0. Each reported row used 150 samples. The benchmark definition was identical on both sides of each comparison.
cargo bench --bench request_lifecycle -- 'nidus (32-route controller app|middleware request context request|api defaults production request|api defaults production with metrics request)' --warm-up-time 2 --measurement-time 5 --sample-size 150 --save-baseline pre_elite_pass
cargo bench --bench request_lifecycle -- 'nidus 64-schema openapi document construction' --warm-up-time 2 --measurement-time 5 --sample-size 150 --save-baseline pre_elite_pass
cargo bench --bench request_lifecycle -- 'nidus (32-route controller app|64-schema openapi document construction)' --warm-up-time 2 --measurement-time 5 --sample-size 150 --baseline pre_elite_pass
| Benchmark | Before | After | Criterion change |
|---|---|---|---|
| 32-route controller construction | 24.195-24.331 us | 15.885-16.102 us | 34.1%-35.8% faster |
| 64-schema OpenAPI construction | 177.04-179.30 us | 14.324-14.425 us | 92.0%-92.1% faster |
The routing change normalizes a controller mount prefix once and joins route paths that were already normalized by RouteDefinition. Path normalization also writes into one pre-sized String rather than allocating a temporary String for every segment plus a Vec and joined output. The OpenAPI change uses the document's owned BTreeMap entry API directly, preserving its existing first-registration-wins rule without cloning accumulated schemas.
A request-context in-place refresh was also tested against the production defaults rows. Repeated comparisons were inconsistent: one isolated run showed a small improvement, while a later run found no significant change without metrics and a 9.8%-15.2% regression with metrics. The experiment was reverted and is not included in this pass.
1.0.8 framework hot-path pass (2026-07-10)
Controller assembly, request-context cloning, and immutable OpenAPI responses were measured with saved Criterion baselines on the same aarch64-apple-darwin machine and rustc 1.96.0. Each row used 150 samples. The new benchmark rows were applied identically to the pre-change and current source trees; the request-stack baseline was rebuilt from the pre-change commit before comparison.
cargo bench --bench request_lifecycle -- 'nidus (hello-world app|controller \+ service app|controller setup|guarded route|rate limit store check with 10k identities)' --sample-size 150 --save-baseline pre_nidus_quality
cargo bench --bench request_lifecycle -- 'nidus (32-route controller app|request context clone|100-route openapi json request)' --sample-size 150 --save-baseline pre_nidus_quality
cargo bench --bench request_lifecycle -- 'nidus (hello-world app|32-route controller app|request context clone|100-route openapi json request)' --sample-size 150 --baseline pre_nidus_quality
cargo bench --bench request_lifecycle -- 'nidus (middleware request context request|api defaults production request|api defaults production with metrics request)' --sample-size 150 --save-baseline pre_context_stack
cargo bench --bench request_lifecycle -- 'nidus (middleware request context request|api defaults production request|api defaults production with metrics request)' --sample-size 150 --baseline pre_context_stack
| Benchmark | Before | After | Criterion change |
|---|---|---|---|
| Hello-world controller construction | 1.9211-1.9452 us | 1.0304-1.0447 us | 45.9%-46.7% faster |
| 32-route controller construction | 52.792-53.242 us | 23.881-23.947 us | 55.1%-56.2% faster |
| Request context clone | 92.385-92.924 ns | 3.6536-3.6717 ns | 96.0%-96.1% faster |
| 100-route OpenAPI JSON response | 188.42-190.58 us | 519.09-520.22 ns | 99.7% faster |
| Request context middleware | 973.37-1009.1 ns | 896.81-901.10 ns | 3.8%-6.0% faster |
| Production defaults | 2.6012-2.6602 us | 2.2908-2.3400 us | 14.7%-17.2% faster |
| Production defaults with metrics | 2.9016-2.9701 us | 2.6952-2.7014 us | 7.1%-8.7% faster |
These are local latency and construction-time measurements, not universal throughput claims. A borrowed-key rate-limit lookup and a request-scope clone avoidance experiment were also measured. The former regressed its benchmark and the latter stayed within Criterion's noise threshold, so both were reverted.
1.0.7 optimization pass (2026-07-09)
The bounded event queue and structured logging changes were measured with saved Criterion baselines on the same aarch64-apple-darwin machine and rustc 1.96.0. The intervals below are Criterion's 100-sample estimates; they are local evidence, not universal throughput claims.
cargo bench --bench event_bus -- 'nidus bounded event publish at 10k capacity' --warm-up-time 2 --measurement-time 5 --sample-size 100 --save-baseline before-event-queue
cargo bench --bench event_bus -- 'nidus bounded event publish at 10k capacity' --warm-up-time 2 --measurement-time 5 --sample-size 100 --baseline before-event-queue
cargo bench --bench request_lifecycle -- '(rate limit store check with 10k identities|structured logging span creation)' --warm-up-time 2 --measurement-time 5 --sample-size 100 --save-baseline before-http-hotpaths
cargo bench --bench request_lifecycle -- 'structured logging span creation' --warm-up-time 2 --measurement-time 5 --sample-size 100 --baseline before-http-hotpaths
| Benchmark | Before | After | Criterion change |
|---|---|---|---|
| Bounded event publish at 10k capacity | 1.0719-1.1939 us | 67.098-78.214 ns | 93.9%-94.7% faster |
| Structured logging span creation | 158.54-159.89 ns | 83.327-86.991 ns | 37.9%-42.0% faster |
The rate-limit row was included in the saved HTTP baseline to evaluate a borrowed-key lookup experiment. That experiment regressed the measured row and was reverted; the rate-limit implementation is unchanged by this pass.
Historical reference
These numbers are one local validation run, not a universal performance claim. They were captured on 2026-06-25 at commit 4d19496 with cargo bench, rustc 1.96.0 (ac68faa20 2026-05-25), aarch64-apple-darwin, and macOS 14.5 (23F79) on arm64 hardware. Criterion reported several outliers and mixed regressions/improvements versus local saved history, including raw or unrelated benchmarks moving in different directions. Treat the table as a current reference point, not publish-grade proof.
The 1.0.6 optimization pass re-measured the rows it changed; see docs/release-1-0-6.md for the per-change before/after figures and methodology (stash-based A/B runs on the same machine). The table below remains a compact reference that mixes the original full-table capture with later follow-up runs and predates the 1.0.6 improvements for the affected rows.
Headline 1.0.6 deltas measured locally on 2026-07-03/04:
- singleton dependency resolution: 11.78 ns -> 3.96 ns
- production default stack request: 2.109 us -> 1.962 us
- Prometheus record response: 126.7 ns -> 81.3 ns; render text: 30.1 us ->
- rate limit store check with 10k identities: 27.54 us -> 32.4 ns
7.2 us
| Benchmark | Central estimate | Notes |
|---|---|---|
| Nidus singleton dependency resolution | 24.944 ns | direct container lookup |
| raw Axum route composition | 1.9600 us | composition baseline |
| Nidus controller route composition | 5.7504 us | controller route builder path |
| raw Axum baseline request | 688.30 ns | request baseline |
| Nidus hello-world request | 716.20 ns | simple controller request |
| Nidus hello-world app | 3.2809 us | app construction microbenchmark |
| Nidus controller + service request | 777.87 ns | injected service route |
| Nidus controller + service app | 3.6975 us | app construction with DI setup |
| Nidus controller setup | 267.71 ns | controller builder setup |
| Nidus guarded route | 1.1572 us | authorization guard layer |
| Nidus validation route | 2.3850 us | validation extractor path |
| Nidus request-scoped route | 1.5848 us | request-scoped provider resolution |
| Nidus middleware security headers request | 1.1748 us | response header layer |
| Nidus middleware body limit request | 858.22 ns | declared Content-Length check |
| Nidus middleware legacy request ID request | 1.4592 us | Wave 28 follow-up run; legacy generated UUID layer |
| Nidus middleware validated request ID request | 1.6663 us | strict UUID request ID layer |
| Nidus middleware request context request | 1.3570 us | request context layer |
| Nidus middleware error envelope success request | 1.0651 us | success path through envelope layer |
| Nidus middleware timeout response request | 982.22 ns | non-timeout success path |
| Nidus API defaults production request | 3.4495 us | production stack without metrics |
| Nidus API defaults production with metrics request | 4.2138 us | production stack with metrics hook |
| Nidus metrics record response | 243.52 ns | in-process Prometheus collector |
| Nidus metrics record inner error | 292.54 ns | error path recording |
| Nidus metrics render text | 53.494 us | renders 10 routes with 100 samples each |
Reading Results
Default request handling does not resolve the dependency graph per request. Request-scoped providers, validation, guards, production defaults, and metrics are opt-in layers with measurable costs. The in-process Prometheus collector is useful for examples, local services, and tests, but high-cardinality route labels increase render output and memory use; prefer stable route patterns such as /users/{id} over concrete IDs.
Before publishing performance claims, rerun these benchmarks on the release machine, include equivalent raw Axum baselines where relevant, preserve Criterion output, and report noise, outliers, and tradeoffs directly.