Benchmarks
Criterion benchmark results for the rsigma detection engine.
Hardware: Apple M4 Pro, macOS
Profile: bench (optimized, release)
Date captured: 2026-07-05
Captured on version: 0.18.0 (main)
All suites below were rerun in full on the date above. To refresh for a specific release, check out the matching tag, run the commands in Running, and update the hardware/date/version block.
Running
# All benchmarks across all crates
cargo bench
# Individual suites
cargo bench -p rsigma-parser --bench parse
cargo bench -p rsigma-eval --bench eval
cargo bench -p rsigma-eval --bench eval --features daachorse-index # includes the cross-rule AC suite
cargo bench -p rsigma-eval --bench logsource
cargo bench -p rsigma-eval --bench schema
cargo bench -p rsigma-eval --bench routing
cargo bench -p rsigma-eval --bench array
cargo bench -p rsigma-eval --bench correlation
cargo bench -p rsigma-eval --bench correlation_memory # peak-heap stress (not Criterion)
cargo bench -p rsigma-eval --bench result_serialize
cargo bench -p rsigma-runtime --bench runtime_throughput
cargo bench -p rsigma-runtime --bench input_formats --features logfmt,cef,evtx
cargo bench -p rsigma-runtime --bench otlp --features otlp
cargo bench -p rsigma-runtime --bench dynamic_pipelines
cargo bench -p rsigma-runtime --bench enrichment
cargo bench -p rsigma-runtime --bench alert_pipeline
cargo bench -p rsigma-runtime --bench risk
# Specific benchmark group
cargo bench -p rsigma-eval --bench eval -- eval_throughput
# Quick mode (fewer samples, useful for CI smoke tests)
cargo bench -- --quick
Parser (rsigma-parser)
Rule Parsing
| Benchmark | Time (median) | Throughput |
|---|---|---|
| Single rule | 10.6 us | - |
| 10 rules | 110.0 us | - |
| 100 rules | 1.18 ms | 2.37 MiB/s |
| 500 rules | 5.81 ms | 5.09 MiB/s |
| 1000 rules | 11.9 ms | 12.4 MiB/s |
| Complex condition (single) | 23.8 us | - |
Wildcard and Regex Rules (pre-compiled pattern cache)
| Benchmark | Time (median) |
|---|---|
| Wildcard rules (100) | 85.8 us |
| Wildcard rules (500) | 88.2 us |
| Wildcard rules (1000) | 85.1 us |
| Regex rules (100) | 51.5 us |
| Regex rules (500) | 50.2 us |
| Regex rules (1000) | 51.6 us |
Wildcard/regex rule parsing is O(1) due to pattern caching (only the first parse compiles the patterns).
Evaluation Engine (rsigma-eval)
Rule Compilation
| Rules | Time (median) |
|---|---|
| 100 | 138.8 us |
| 500 | 620.9 us |
| 1,000 | 1.16 ms |
| 5,000 | 6.12 ms |
Rule Load Paths
Compares the three engine entry points for loading rules at large N. add_collection and add_rules rebuild the inverted and bloom indexes once at the end of the batch; add_rule in a loop folds each rule incrementally with an amortized-doubling bloom rebuild (64-rule floor, 2x ratchet).
| Rules | add_collection |
add_rules |
add_rule loop |
|---|---|---|---|
| 1,000 | 1.21 ms (1.21 us/rule) | 1.18 ms (1.18 us/rule) | 1.63 ms (1.63 us/rule) |
| 10,000 | 11.94 ms (1.19 us/rule) | 12.04 ms (1.20 us/rule) | 17.99 ms (1.80 us/rule) |
| 100,000 | 131.44 ms (1.31 us/rule) | 131.17 ms (1.31 us/rule) | 176.26 ms (1.76 us/rule) |
All three paths scale linearly in the rule count. Per-rule cost is essentially constant from 1K to 100K, confirming the O(N) total complexity:
add_collectionandadd_rulescost roughly 1.2-1.3 us/rule. The fixed per-batch cost is dominated by the final inverted index + bloom build over the aggregate.add_rulein a loop costs roughly 1.6-1.8 us/rule, about 40% more than the batched paths. The overhead is the per-rule incremental insert plus the ~11 doubling-watermark rebuilds the bloom triggers between 1 and 100K rules. There is no quadratic blowup; the constant factor pays for the incremental contract.
The takeaway is that add_rule is not a foot-gun for bulk loads. Batched APIs are still slightly faster and remain the recommended path for cold-load scenarios; the single-rule path exists for cases where the caller wants per-rule error reporting (rsigma rule validate) or per-rule mutation semantics.
Run with cargo bench -p rsigma-eval --bench eval -- rule_load.
Single Event Evaluation
Time to evaluate one event against N compiled rules.
| Rules | Time (median) | Per-rule |
|---|---|---|
| 100 | 2.55 us | 25.5 ns |
| 500 | 14.2 us | 28.4 ns |
| 1,000 | 31.8 us | 31.8 ns |
| 5,000 | 168.6 us | 33.7 ns |
Detection Throughput (100 rules)
| Events | Time (median) | Throughput |
|---|---|---|
| 1,000 | 2.71 ms | 370 Kelem/s |
| 10,000 | 27.4 ms | 364 Kelem/s |
| 100,000 | 276.4 ms | 362 Kelem/s |
Batch Mode (Sequential vs Parallel)
| Configuration | Time (median) | Throughput |
|---|---|---|
| 100 rules, sequential | 2.73 ms | 367 Kelem/s |
| 100 rules, batch | 2.73 ms | 366 Kelem/s |
| 1000 rules, sequential | 33.9 ms | 29.5 Kelem/s |
| 1000 rules, batch | 34.5 ms | 28.9 Kelem/s |
| 5000 rules, sequential | 182.1 ms | 5.49 Kelem/s |
| 5000 rules, batch | 244.8 ms | 4.09 Kelem/s |
Wildcard and Regex Matching
| Benchmark | Time (median) |
|---|---|
| Wildcard (100 rules) | 21.0 us |
| Wildcard (500 rules) | 20.9 us |
| Wildcard (1000 rules) | 20.9 us |
| Regex (100 rules) | 5.50 us |
| Regex (500 rules) | 5.51 us |
| Regex (1000 rules) | 5.55 us |
Wildcard/regex matching scales O(1) with rule count thanks to compiled pattern sets.
Aho-Corasick Threshold Sweep
Single rule with N |contains patterns evaluated against 50 randomly generated events at varying haystack lengths. Drove the choice of AHO_CORASICK_THRESHOLD = 8 in compiler/optimizer.rs. Throughput is per event.
| Patterns | h=100 B | h=1 KB | h=8 KB | h=64 KB |
|---|---|---|---|---|
| 1 | 8.34 Melem/s (5.99 us / batch) | 4.99 Melem/s (10.0 us) | 1.11 Melem/s (45.1 us) | 67.8 Kelem/s (737.9 us) |
| 2 | 5.91 Melem/s (8.46 us) | 1.60 Melem/s (31.2 us) | 195 Kelem/s (256.1 us) | 18.0 Kelem/s (2.77 ms) |
| 4 | 4.10 Melem/s (12.2 us) | 1.09 Melem/s (45.9 us) | 93.9 Kelem/s (532.3 us) | 10.1 Kelem/s (4.96 ms) |
| 8 | 5.44 Melem/s (9.19 us) | 639 Kelem/s (78.2 us) | 82.2 Kelem/s (608.2 us) | 10.2 Kelem/s (4.90 ms) |
| 16 | 5.32 Melem/s (9.39 us) | 641 Kelem/s (78.0 us) | 82.3 Kelem/s (607.9 us) | 10.1 Kelem/s (4.94 ms) |
| 32 | 5.19 Melem/s (9.64 us) | 638 Kelem/s (78.4 us) | 79.3 Kelem/s (630.6 us) | 9.27 Kelem/s (5.39 ms) |
Throughput flattens at p=8: p16 and p32 perform within ~3% of p8 because the AC automaton scans the haystack once regardless of pattern count. Below 8 patterns, the sequential str::contains path with SIMD acceleration (memchr / Two-Way) wins on longer haystacks; at p8 the automaton already beats the p4 sequential path on short haystacks. The crossover stands at 8.
Run with cargo bench -p rsigma-eval --bench eval -- eval_ac_threshold_sweep.
Bloom Pre-Filter (--bloom-prefilter)
200 non-matching events evaluated against N substring-heavy rules with the bloom prefilter off vs on (Engine::set_bloom_prefilter(true)).
| Rules | Off | On | Speedup |
|---|---|---|---|
| 100 | 6.62 ms (151 Kelem/s) | 6.65 ms (150 Kelem/s) | ~1.0x |
| 500 | 43.7 ms (22.9 Kelem/s) | 38.6 ms (25.9 Kelem/s) | ~1.13x |
| 1,000 | 88.6 ms (11.3 Kelem/s) | 74.4 ms (13.4 Kelem/s) | ~1.19x |
| 5,000 | 442.6 ms (2.26 Kelem/s) | 365.7 ms (2.73 Kelem/s) | ~1.21x |
The bloom filter pays off from roughly 500 rules upward on non-matching traffic and is neutral below that. It never slows evaluation down in this workload, but it is opt-in because matching-heavy traffic pays the filter cost without the skip benefit.
Run with cargo bench -p rsigma-eval --bench eval -- eval_bloom_rejection.
Cross-Rule Aho-Corasick Pre-Filter, daachorse-index feature
200 non-matching events evaluated against N pure-substring rules. Best-case workload for the cross-rule index: every rule is AC-prunable (every detection consists exclusively of positive substring matchers, no negation in conditions), and every event has zero pattern hits across all fields.
| Rules | Off (default) | On (set_cross_rule_ac(true)) |
Speedup |
|---|---|---|---|
| 1,000 | 17.50 ms (11.4 Kelem/s) | 288.8 us (693 Kelem/s) | ~61x |
| 5,000 | 87.63 ms (2.28 Kelem/s) | 1.05 ms (191 Kelem/s) | ~84x |
| 10,000 | 186.92 ms (1.07 Kelem/s) | 2.05 ms (97.6 Kelem/s) | ~91x |
The cross-rule index turns O(rules × patterns) per event into O(haystack_length) for the AC scan, so throughput is essentially constant in rule count once the index is enabled.
For typical mixed workloads (substring + exact + regex rules, events that hit multiple fields, smaller rule sets) the index adds build-time and lookup overhead with smaller wins or none, and can even cause a slowdown. Off by default. Enable via Engine::set_cross_rule_ac(true) programmatically, or --cross-rule-ac on rsigma engine daemon / rsigma engine eval (requires the daachorse-index Cargo feature). Always benchmark against representative data before flipping it on.
Run with cargo bench -p rsigma-eval --features daachorse-index --bench eval -- eval_cross_rule_ac.
Logsource Pruning (--logsource-routing)
Single-event evaluation over an always-evaluated (contains-only, never-matching) ruleset split 50/50 across product: windows and product: linux, with a windows-tagged event. With pruning on, the conflicting-product half is never iterated, so the win tracks the pruned fraction.
| Rules | Off | On | Speedup |
|---|---|---|---|
| 1,000 | 112.2 us | 64.1 us | ~1.75x |
| 10,000 | 1.19 ms | 678.1 us | ~1.76x |
Run with cargo bench -p rsigma-eval --bench logsource.
Schema Classification (engine classify, --schema-routing, --observe-schemas)
Per-event cost of SchemaClassifier::classify against the built-in signature set. This is the hot-path overhead schema routing and schema observability add per event.
| Event | Time (median) |
|---|---|
| ECS Windows (early match, highest specificity) | 289 ns |
| Sysmon flat (mid-list match) | 443 ns |
| OCSF | 216 ns |
| Unknown (full signature scan, worst case) | 548 ns |
| ECS Windows with ambiguity check | 296 ns |
Classification stays well under a microsecond per event even in the full-scan worst case, so --observe-schemas and --schema-routing cost a fraction of a percent at typical pipeline throughputs.
Run with cargo bench -p rsigma-eval --bench schema.
Schema-Routed Dispatch (--schema-routing)
End-to-end routed evaluation over a mixed stream (one third ECS, one third flat Sysmon, one third unrecognized; 1,000 events, 100 non-matching rules), with ecs and sysmon bound to their builtin pipelines, against a single pipeline-less engine over the same stream.
| Configuration | Time (median) | Throughput |
|---|---|---|
| Single engine, no pipelines | 5.07 ms | 197 Kelem/s |
Routed, per-event route() |
9.34 ms | 107 Kelem/s |
Routed, process_batch |
9.45 ms | 106 Kelem/s |
The ~4.3 us/event difference overstates the dispatch cost: classification itself is ~0.3 us (see above), and most of the rest is the routed engines doing real matching work, because their pipeline-mapped rules reference fields the events actually carry (process.command_line), while the unrouted baseline’s unmapped rules miss ECS events on absent fields without ever scanning a value. That is the correctness gap routing exists to close: the baseline is faster partly because it silently cannot match two thirds of the stream.
Run with cargo bench -p rsigma-eval --bench routing.
Array Matching (sigma-version: 3)
Per-event cost of the array evaluation paths. Events carry a connections array of objects; the flat baseline is the same engine evaluating a single scalar field (326 ns). Array-scope bodies are evaluated per member rather than through the batched flat-field matchers, so cost is linear in member count.
| Path | 10 members | 100 members | 1,000 members |
|---|---|---|---|
| Implicit any-member, hit at last element | 1.80 us | 12.4 us | 113.2 us |
| Implicit any-member, miss (full scan) | 564 ns | 3.78 us | 34.6 us |
[any] scope, hit at first element |
2.47 us | 22.2 us | 220.2 us |
[any] scope, hit at last element |
2.75 us | 24.9 us | 245.3 us |
[any] scope, miss |
635 ns | 5.80 us | 57.8 us |
[all] scope, every member satisfies |
2.68 us | 24.8 us | 247.1 us |
[all] scope, first member fails |
391 ns | 3.08 us | 31.4 us |
Positional indexing is O(1) in the array length: args[0] costs 693 ns and args[-1] 671 ns against a 1,000-element array, about 2x the flat baseline for the index resolution.
Two properties fall out of the numbers. Non-firing evaluation costs ~35-60 ns per member (a 100-member [any] miss is 5.8 us, ~18x the flat baseline), and [all] exits early on the first failing member. But a firing rule pays ~110-250 ns per member regardless of where the matching element sits: [any] hit-first costs nearly the same as hit-last because the fan-out continues past the first hit to collect every matching member for match details. Budget array rules by array length times rule count on matching traffic, not by early-exit intuition.
Run with cargo bench -p rsigma-eval --bench array.
Result Serialization
Serializing EvaluationResult to the flat NDJSON wire shape, comparing the derive-based baseline against a #[serde(flatten)] variant and a hand-written Serialize impl.
| Payload | v1 baseline | v2 flatten derive | v3 hand-written |
|---|---|---|---|
| Small detection | 136 ns | 135 ns | 135 ns |
| Realistic detection | 481 ns | 477 ns | 479 ns |
| Small correlation | 177 ns | 182 ns | 180 ns |
| Realistic correlation | 1.72 us | 1.72 us | 1.73 us |
All three implementations are within noise of each other; a hand-written serializer buys nothing, so the derive stays. Even a realistic correlation result serializes in under 2 us, an order of magnitude below its evaluation cost.
Run with cargo bench -p rsigma-eval --bench result_serialize.
Correlation Engine (rsigma-eval)
Event Count Correlation
1000 events evaluated against N correlation rules.
| Corr rules | Time (median) | Throughput |
|---|---|---|
| 5 | 1.10 ms | 907 Kelem/s |
| 10 | 1.09 ms | 915 Kelem/s |
| 20 | 1.12 ms | 894 Kelem/s |
Temporal Correlation
1000 events evaluated with temporal ordering constraints.
| Corr rules | Time (median) | Throughput |
|---|---|---|
| 3 | 567.4 us | 1.76 Melem/s |
| 5 | 569.4 us | 1.76 Melem/s |
| 10 | 569.6 us | 1.76 Melem/s |
Correlation Throughput
| Events | Time (median) | Throughput |
|---|---|---|
| 10,000 | 20.1 ms | 497 Kelem/s |
| 100,000 | 199.6 ms | 501 Kelem/s |
Sequential vs Batch (10,000 events)
| Mode | Time (median) | Throughput |
|---|---|---|
| Sequential | 20.0 ms | 499 Kelem/s |
| Batch | 21.7 ms | 462 Kelem/s |
State Pressure (unique group-by keys)
| Unique keys | Time (median) | Throughput |
|---|---|---|
| 1,000 | 795.2 us | 1.26 Melem/s |
| 10,000 | 8.15 ms | 1.23 Melem/s |
| 50,000 | 41.4 ms | 1.21 Melem/s |
Window Modes: sliding vs tumbling vs session
Identical event_count workload for all three modes: 10,000 events across 1,000 group keys, one event per group per 10s tick, 1h window, 10m session gap. The window decision in apply_window_open is O(1) (deque front/back inspection), so the three modes cost the same per event.
| Window mode | Time (median) | Throughput |
|---|---|---|
sliding (default) |
8.12 ms | 1.23 Melem/s |
tumbling |
7.77 ms | 1.29 Melem/s |
session |
7.91 ms | 1.26 Melem/s |
Run with cargo bench -p rsigma-eval --bench correlation -- correlation_window_modes.
Window-Mode Memory Stress
The correlation_memory bench is not a Criterion suite: it installs a counting global allocator and reports peak and settled heap deltas over the engine baseline, isolating window-state maintenance (alert thresholds are unreachable; event construction is excluded from the deltas). It reproduces the two scenarios from the SEP #214 discussion on memory becoming the bottleneck in stateful window correlation.
A. High-cardinality session windows (one event per unique key, event_count, gap 5m, cap 2h), exercising the max_state_entries hard cap and stalest-first eviction:
| Configuration | Throughput | Peak heap | Settled | Live groups |
|---|---|---|---|---|
| 100k keys, default cap (100k) | 756 Kelem/s | 20.5 MiB | 17.7 MiB | 100,000 |
| 1M keys, default cap (100k) | 841 Kelem/s | 39.8 MiB | 22.4 MiB | capped |
| 1M keys, cap raised to 2M | 742 Kelem/s | 327.4 MiB | 243.8 MiB | 1,000,000 |
A live session group costs ~256 B settled, dominated by the GroupKey heap strings rather than the timestamps. Throughput under active eviction is the highest of the three runs because the state map stays small; the eviction sort is amortized over the 10% headroom the cap reclaims.
B. Long-lived chatty sessions (groups emitting continuously inside an open session; gap never exceeded, so the per-group deque grows to timespan/interval entries):
| Workload | Throughput | Peak heap | Bytes/in-window event |
|---|---|---|---|
event_count session, 1k groups @ 30s (240 ev/window) |
1.08 Melem/s | 2.2 MiB | ~10 B |
event_count sliding, 1k groups @ 30s (240 ev/window) |
1.06 Melem/s | 2.2 MiB | ~10 B |
value_count session, 1k groups @ 30s, distinct strings |
306 Kelem/s | 21.1 MiB | ~92 B |
event_count session, 100 groups @ 1 ev/s (7,200 ev/window) |
1.10 Melem/s | 6.3 MiB | ~9 B |
value_count session, 100 groups @ 1 ev/s, distinct (1,800 ev/window) |
57 Kelem/s | 16.0 MiB | ~93 B |
C. Mode comparison (10k groups, 1M events, 1h window): sliding 915 Kelem/s, tumbling 959 Kelem/s, session 997 Kelem/s, all at a 6.6 MiB peak. Memory differences between modes come only from how long entries are retained, not from per-event overhead.
Run with cargo bench -p rsigma-eval --bench correlation_memory (about half a minute total).
Runtime (rsigma-runtime)
LogProcessor Pipeline Throughput
End-to-end processing: format parsing, detection, and result collection (100 rules).
| Format | Events | Time (median) | Throughput |
|---|---|---|---|
| JSON | 1,000 | 1.14 ms | 880 Kelem/s |
| JSON | 10,000 | 8.94 ms | 1.12 Melem/s |
| Syslog | 1,000 | 744.3 us | 1.34 Melem/s |
| Syslog | 10,000 | 6.35 ms | 1.57 Melem/s |
| Plain | 1,000 | 184.4 us | 5.42 Melem/s |
| Plain | 10,000 | 1.05 ms | 9.52 Melem/s |
| Auto-detect | 1,000 | 1.07 ms | 939 Kelem/s |
| Auto-detect | 10,000 | 8.86 ms | 1.13 Melem/s |
Feature-Gated Input Formats (100 rules)
logfmt and CEF run through the same LogProcessor pipeline as the table above; EVTX measures EvtxFileReader binary-record parsing over the checked-in 2 MiB security.evtx fixture (~2,260 records), the dominant cost of engine eval -e @file.evtx.
| Format | Events | Time (median) | Throughput |
|---|---|---|---|
| logfmt | 1,000 | 1.83 ms | 546 Kelem/s |
| logfmt | 10,000 | 15.8 ms | 631 Kelem/s |
| CEF | 1,000 | 2.11 ms | 473 Kelem/s |
| CEF | 10,000 | 19.0 ms | 527 Kelem/s |
| EVTX (parse only) | ~2,260 records | 11.6 ms | 195 Kelem/s |
Run with cargo bench -p rsigma-runtime --bench input_formats --features logfmt,cef,evtx.
OTLP Log Decode (otlp feature)
logs_request_to_raw_events: flattening an OTLP ExportLogsServiceRequest (string body, eight log attributes, four resource attributes, scope metadata, trace context per record) into the JSON events the engine evaluates. This is the ingest-side cost of OTLP/HTTP and OTLP/gRPC downstream of transport and protobuf decoding.
| Records | Time (median) | Throughput |
|---|---|---|
| 100 | 225.4 us | 444 Kelem/s |
| 1,000 | 2.27 ms | 441 Kelem/s |
| 10,000 | 22.8 ms | 439 Kelem/s |
Decode costs a flat ~2.3 us per record independent of batch size, comparable to the JSON-line parse-plus-detect cost, so OTLP ingestion roughly halves single-core throughput relative to NDJSON input.
Run with cargo bench -p rsigma-runtime --bench otlp --features otlp.
Field Observability (--observe-fields)
The same 10,000-event JSON workload as above (seven fields per event, 100 rules) with the field observer off vs on.
| Mode | Time (median) | Throughput |
|---|---|---|
| Off | 8.58 ms | 1.17 Melem/s |
| On | 11.5 ms | 867 Kelem/s |
The observer adds ~0.3 us per event on seven-key events (a mutex-guarded counter update per field key), about 25% of this workload’s total pipeline cost. Cheap in absolute terms, but it is the most expensive of the opt-in per-event observability features and scales with the event’s key count.
Run with cargo bench -p rsigma-runtime --bench runtime_throughput -- runtime_observe_fields.
Raw Engine vs LogProcessor (10,000 events, 100 rules)
| Mode | Time (median) | Throughput |
|---|---|---|
| Raw Engine (pre-parsed) | 11.3 ms | 884 Kelem/s |
| LogProcessor (JSON) | 8.70 ms | 1.15 Melem/s |
| LogProcessor (auto-detect) | 8.73 ms | 1.15 Melem/s |
Rule Scaling (1,000 JSON events)
| Rules | Time (median) | Throughput |
|---|---|---|
| 100 | 1.04 ms | 959 Kelem/s |
| 500 | 1.04 ms | 957 Kelem/s |
| 1,000 | 1.04 ms | 961 Kelem/s |
Rule count has minimal impact on runtime throughput due to the engine’s indexed matching.
Post-Engine Layers (rsigma-runtime)
Sink-path stages that run after evaluation and before delivery. All figures are per 1,000-result batch; cardinality is the number of distinct entity/fingerprint values cycling through the batch.
Enrichment (template primitive)
The CPU-only floor cost of the enrichment pipeline: template interpolation, kind/scope filtering, semaphore acquisition, and the enrichments-map injection. I/O-bound primitives (http, command, lookup) are dominated by their fetch latency and response cache, not the pipeline.
| Enrichers | Time (median) | Per result |
|---|---|---|
| 1 | 857.0 us | ~0.86 us |
| 4 | 2.56 ms | ~0.64 us per enricher |
Run with cargo bench -p rsigma-runtime --bench enrichment.
Alert Pipeline (dedup + incident grouping)
| Entity cardinality | Time (median) | Per result |
|---|---|---|
| 1 | 435.2 us | ~0.44 us |
| 10 | 449.6 us | ~0.45 us |
| 100 | 564.2 us | ~0.56 us |
Dedup folding plus incident grouping cost roughly half a microsecond per result and grow gently with fingerprint cardinality (more open alerts and incidents to track).
Run with cargo bench -p rsigma-runtime --bench alert_pipeline.
Risk Layer (annotation + per-entity accumulation)
| Entity cardinality | Time (median) | Per result |
|---|---|---|
| 1 | 3.87 ms | ~3.9 us |
| 10 | 1.21 ms | ~1.2 us |
| 100 | 1.09 ms | ~1.1 us |
Single-entity is the worst case, inverted from the alert pipeline: every result accrues into one entity whose window deque keeps growing and whose accumulated risk repeatedly crosses the incident threshold. At realistic cardinalities the layer costs about a microsecond per result.
Run with cargo bench -p rsigma-runtime --bench risk.
Dynamic Pipelines (rsigma-runtime)
Source Resolution (File I/O + JSON Parse)
| Items | Time (median) |
|---|---|
| 10 | 17.4 us |
| 100 | 20.5 us |
| 1,000 | 61.6 us |
| 10,000 | 441.8 us |
Data Parsing (No I/O)
| Format | Items | Time (median) |
|---|---|---|
| JSON | 10 | 366 ns |
| JSON | 100 | 2.78 us |
| JSON | 1,000 | 24.3 us |
| JSON | 10,000 | 239.0 us |
| YAML | 10 | 3.46 us |
| Lines | 100 | 2.83 us |
Extract Expressions
Expression evaluation on a 100-item dataset with nested objects.
| Language | Expression type | Time (median) |
|---|---|---|
| JQ | Identity (.items) |
206.5 us |
| JQ | Filter (select(.active)) |
284.2 us |
| JQ | Nested path (.a.b.c) |
177.9 us |
| JSONPath | Simple ($.items[*].name) |
22.5 us |
| JSONPath | Filter ([?@.active==true]) |
23.6 us |
| CEL | Field access (data.metadata.count) |
54.1 us |
| CEL | List filter (.filter(x, x.active)) |
759.3 us |
JQ times rose roughly 3x against the 0.9.0 baseline following the jaq 1.x to 3.0 migration (0.13.0, Radically Open Security audit fixes); JSONPath is unaffected and now ~10x faster than JQ for comparable filters.
Template Expansion
TemplateExpander::expand substituting ${source.*} references in pipeline vars.
| Vars | Values/source | Time (median) |
|---|---|---|
| 1 | 10 | 487 ns |
| 5 | 10 | 2.17 us |
| 10 | 10 | 4.24 us |
| 20 | 10 | 8.54 us |
| 5 | 100 | 10.1 us |
| 5 | 1,000 | 91.3 us |
Resolve with Extract (File + Filter, 500 IOC entries)
| Language | Time (median) |
|---|---|
JQ (.indicators[] | select(.active) | .value) |
943.8 us |
JSONPath ($.indicators[?@.active==true].value) |
253.4 us |
CEL (data.indicators.filter(x, x.active).map(x, x.value)) |
39.7 ms |
Dynamic Detection End-to-End
Full pipeline: resolve source, expand templates, apply value_placeholders, evaluate events.
| Scenario | Time (median) | Throughput |
|---|---|---|
| Detect 1000 events (50 IOCs) | 351.3 us | 2.85 Melem/s |
| Reload with resolve | 175.4 us | - |
Reload now includes the fail-closed dynamic-source re-resolution that load_rules performs since 0.14.0, which is why it costs more than the 0.9.0 baseline measured without it. In production, reload latency is dominated by source fetch time anyway.
Key Observations
- AC threshold is empirically 8: substring-list throughput flattens at 8 patterns once Aho-Corasick takes over. p16/p32 perform within ~3% of p8; below 8, the sequential
str::containsSIMD path (memchr / Two-Way) is faster on longer haystacks. - Cross-rule AC is order-of-magnitude on substring-only rule sets: with the
daachorse-indexfeature enabled, 200 non-matching events against 10K pure-substring rules dropped from 187 ms to 2.05 ms (~91x). Off by default; only worth enabling for substring-heavy rule sets where most events don’t match (e.g., threat-intel feeds against high-volume telemetry). - The bloom prefilter pays off from ~500 rules: ~20% faster on non-matching traffic at 1K-5K rules, neutral at 100. Opt-in because matching-heavy traffic pays the filter cost without the skip benefit.
- Logsource pruning wins track the pruned fraction: a 50/50 two-product split evaluates ~1.75x faster with
--logsource-routingon, at both 1K and 10K rules. - Schema classification is sub-microsecond: 216-548 ns per event against the full built-in signature set, so schema routing and observability are effectively free at pipeline throughputs.
- Array matching is linear in member count, and hits cost more than misses: non-firing paths cost ~35-60 ns per member while a firing rule pays ~110-250 ns per member independent of match position, because the fan-out collects every matching member for match details. Positional indexing (
args[N]) stays O(1). - Detection is fast: ~365K events/sec with 100 rules in pure evaluation mode, scaling linearly with event count; the full JSON runtime pipeline reaches 1.12M events/sec.
- Runtime overhead is negative: LogProcessor with JSON batching is faster than raw Engine evaluation due to batch-level optimizations and format-aware parsing.
- Rule count scales well: runtime throughput is flat from 100 to 1,000 rules thanks to indexed field matching.
- Correlation is efficient: temporal correlations (1.76M elem/s) are ~2x faster than event-count correlations (~900K elem/s), and both scale linearly with events; the mixed-workload pipeline sustains ~500K events/sec.
- Window modes cost the same per event: sliding, tumbling, and session all run at ~1.2-1.3 Melem/s on an identical workload. The window decision is O(1); choosing
sessionoverslidingis free at evaluation time. - Correlation memory is bounded by entry count, not bytes: the
max_state_entriescap (default 100k) held 1M unique session keys to a 39.8 MiB peak via stalest-first eviction. Within the cap, per-group deques grow with timespan x event rate: ~10 B per in-window event forevent_count, ~92 B forvalue_countwith distinct string values. value_countdistinct counting is the correlation bottleneck: the distinct count is recomputed per event over the whole window (O(W) per event), so throughput drops from ~1.1 Melem/s to 57 Kelem/s at 1,800 distinct values per window; CPU collapses before memory does. Prefer shorter windows orevent_countwhere distinctness is not required.- Every wire format lands within one order of magnitude: JSON 1.1M, logfmt 631K, CEF 527K, and OTLP decode 440K events/sec per core, with EVTX binary parsing at 195K records/sec. Format choice moves single-core ingest capacity by at most ~2.5x.
--observe-fieldsis the priciest per-event observability opt-in: ~0.3 us per event on seven-key events (~25% of the JSON pipeline cost on this workload), versus effectively free schema observation. It scales with event key count.- Routed evaluation costs less than it looks: the ~2x gap versus an unrouted engine is mostly the routed engines doing real matching on pipeline-mapped fields the unmapped baseline can never match. Pure dispatch (classify + route) is sub-microsecond.
- Post-engine layers cost about a microsecond per result: template enrichment ~0.9 us, alert-pipeline dedup + grouping ~0.5 us, risk accumulation ~1.1 us at realistic entity cardinalities. At 10K detections/sec the full opt-in sink path consumes under 3% of a core.
- Result serialization is not worth hand-optimizing: the derive-based serializer matches a hand-written impl within noise; even realistic correlation results serialize in under 2 us.
- Template expansion is negligible: even with 20 vars, expansion adds < 10 us. Not a bottleneck.
- JSONPath is the fastest extraction language: ~10x faster than JQ for comparable filter operations since the jaq 3.0 migration, with none of CEL’s interpretation overhead.
- CEL has high overhead: ~150x slower than JSONPath for list filtering. Best suited for simple field access or small datasets.
- Dynamic pipelines add no per-event cost: once the engine is built, detection throughput with dynamic pipelines (2.85M elem/s) is comparable to static pipeline performance.
For operator-facing performance guidance (when to enable --bloom-prefilter and --cross-rule-ac, how to tune --batch-size and --buffer-size), see Performance Tuning. For the metrics that surface throughput and back-pressure at runtime, see Prometheus metrics and Observability.