Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Performance Results

Measured performance for the hbac-rs, abac-rs, and ldap-acis evaluation engines across rule counts from 100 to 1,000,000. All numbers are single-threaded, release-mode, on synthetic workloads using the sssd-prod distribution preset.

Test Environments

ARM64 — Apple M4

  • Platform: macOS 15.4 (ARM64)
  • CPU: Apple M4 (10 cores, single-threaded benchmarks)
  • Rust: 1.96.0
  • Build: cargo build --release (opt-level=3)

x86_64 — Intel Core i7-12800H

  • Platform: Linux 6.15.8 (Fedora 42)
  • CPU: 12th Gen Intel Core i7-12800H (14 cores, single-threaded benchmarks)
  • Build: cargo build --release (opt-level=3)

Performance is platform-dependent. Always benchmark on your target hardware.

Note: These results reflect the latest optimizations. See the Cross-BAC Benchmarking guide for detailed optimization journey and cross-engine comparisons.

All engines use AHash (fast non-cryptographic hashing) for all internal hash operations.


HBAC Performance

Cached (LRU-warm)

10,000 matching requests after 1,000 warmup requests. The LRU cache dominates.

ARM64 (Apple M4)

RulesThroughputMeanP95P99Memory
1007.85M r/s127 ns208 ns292 ns34 KB
1,0005.49M r/s182 ns292 ns375 ns334 KB
10,0004.07M r/s245 ns375 ns417 ns3.3 MB
20,0003.36M r/s298 ns459 ns542 ns6.6 MB
50,0001.68M r/s596 ns1.00 µs1.21 µs16.5 MB
100,000947K r/s1.06 µs1.83 µs2.21 µs33 MB
250,000428K r/s2.34 µs4.17 µs5.00 µs82.5 MB
500,000215K r/s4.64 µs8.33 µs9.96 µs165 MB
1,000,000109K r/s9.14 µs16.5 µs19.8 µs330 MB

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99Memory
10016.0M r/s62 ns71 ns77 ns34 KB
1,00015.1M r/s66 ns75 ns82 ns334 KB
10,00013.9M r/s72 ns81 ns88 ns3.2 MB
20,00013.9M r/s72 ns81 ns87 ns6.4 MB
50,00014.1M r/s71 ns81 ns86 ns16.1 MB
100,00011.5M r/s87 ns99 ns106 ns32.3 MB
250,00011.7M r/s86 ns99 ns106 ns80.7 MB
500,0007.74M r/s129 ns151 ns166 ns161.5 MB
1,000,00011.3M r/s88 ns102 ns109 ns323.1 MB

check_access (LRU-warm)

check_access() returns bool instead of HbacEvaluationResult, avoiding the 3×Vec<String> allocation overhead.

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99
10019.2M r/s52 ns60 ns66 ns
1,00016.8M r/s60 ns70 ns78 ns
10,00015.9M r/s63 ns73 ns78 ns
20,00016.6M r/s60 ns70 ns75 ns
50,00013.8M r/s73 ns86 ns91 ns
100,00013.2M r/s76 ns88 ns95 ns
250,00013.6M r/s74 ns84 ns90 ns
500,00013.0M r/s77 ns90 ns98 ns
1,000,00012.7M r/s79 ns92 ns101 ns

Uncached

10,000 unique non-matching requests, 0 warmup. Every request exercises the full evaluation path (Bloom filter, decision tree, deny-rule index). The first request at 10K+ triggers an index rebuild, creating a bimodal distribution (P95/P99 reflect steady-state; the mean includes the cold first request).

ARM64 (Apple M4)

RulesThroughputMeanP95P99
10011.0M r/s91 ns166 ns167 ns
1,0008.15M r/s123 ns167 ns208 ns
10,0006.40M r/s156 ns125 ns167 ns
20,0003.88M r/s258 ns166 ns167 ns
50,0002.03M r/s492 ns125 ns167 ns
100,0001.06M r/s941 ns167 ns209 ns
250,000449K r/s2.23 µs250 ns292 ns
500,000223K r/s4.48 µs375 ns417 ns
1,000,000106K r/s9.42 µs625 ns708 ns

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99
10011.2M r/s89 ns118 ns174 ns
1,0006.50M r/s154 ns195 ns237 ns
10,0002.50M r/s400 ns302 ns379 ns
20,0001.27M r/s787 ns374 ns463 ns
50,000373K r/s2.68 µs908 ns1.21 µs
100,000249K r/s4.02 µs684 ns828 ns
250,00090K r/s11.1 µs786 ns946 ns
500,00047K r/s21.3 µs971 ns1.15 µs
1,000,00023K r/s43.7 µs1.25 µs1.44 µs

Mixed Workload (Throughput)

80% matching + 20% non-matching, cache enabled, 1,000 warmup.

ARM64 (Apple M4)

RulesThroughputMeanP95P99
1009.26M r/s108 ns167 ns250 ns
1,0006.37M r/s157 ns250 ns292 ns
10,0005.55M r/s180 ns292 ns334 ns
20,0003.65M r/s274 ns459 ns542 ns
50,0001.91M r/s523 ns1.00 µs1.21 µs
100,0001.05M r/s955 ns1.96 µs2.33 µs
250,000485K r/s2.06 µs4.33 µs5.21 µs
500,000249K r/s4.02 µs8.46 µs10.2 µs
1,000,000129K r/s7.74 µs16.4 µs19.7 µs

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99
10015.6M r/s64 ns73 ns78 ns
1,00014.8M r/s68 ns79 ns86 ns
10,00011.3M r/s88 ns101 ns111 ns
20,00011.7M r/s86 ns103 ns112 ns
50,00011.0M r/s91 ns108 ns122 ns
100,0009.58M r/s104 ns125 ns140 ns
250,0009.31M r/s107 ns130 ns189 ns
500,0009.87M r/s101 ns120 ns143 ns
1,000,0009.17M r/s109 ns131 ns176 ns

ABAC Performance

With AHash optimization and bitmap-based deny indexing, ABAC delivers peak throughput at 1K–10K rules and scales inversely with deny rule count beyond that. On x86_64, throughput ranges from 5.7M r/s at 1K rules to 143K r/s at 1M rules, while using 4.2× less memory than HBAC. HBAC now uses the same bitmap deny index optimization and is faster at all scales, but ABAC provides N-dimensional flexibility.

Cached (Single Request Latency)

ARM64 (Apple M4)

RulesThroughputMeanP95P99Memory
1001.48M r/s0.67µs1.46µs2.17µs7.8 KB
1,0003.19M r/s0.31µs0.42µs0.50µs78 KB
10,0003.73M r/s0.27µs0.38µs0.46µs781 KB
20,0003.06M r/s0.33µs0.50µs0.62µs1.6 MB
50,0001.70M r/s0.59µs1.04µs1.29µs3.9 MB
100,000969K r/s1.03µs1.92µs2.42µs7.8 MB
250,000435K r/s2.30µs4.38µs5.38µs19.5 MB
500,000223K r/s4.48µs8.67µs10.9µs39 MB
1,000,000113K r/s8.84µs17.2µs21.3µs78 MB

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99Memory
1002.22M r/s450 ns939 ns1.14 µs7.8 KB
1,0005.69M r/s176 ns227 ns247 ns78 KB
10,0003.97M r/s252 ns348 ns398 ns781 KB
20,0002.88M r/s348 ns559 ns710 ns1.5 MB
50,0001.67M r/s598 ns974 ns1.19 µs3.8 MB
100,000949K r/s1.05 µs1.73 µs2.15 µs7.6 MB
250,000546K r/s1.83 µs3.18 µs4.24 µs19.1 MB
500,000289K r/s3.46 µs6.16 µs9.22 µs38.1 MB
1,000,000143K r/s7.01 µs12.9 µs15.4 µs76.3 MB

Uncached

10,000 unique non-matching requests, 0 warmup. The first request triggers compiled evaluator construction, dominating the mean latency. P95/P99 reflect steady-state uncached performance.

ARM64 (Apple M4)

RulesThroughputMeanP95P99
100930K r/s1.08µs1.21µs1.33µs
1,0003.78M r/s0.26µs0.21µs0.25µs
10,0001.40M r/s0.72µs0.17µs0.21µs
20,000671K r/s1.49µs0.17µs0.25µs
50,000269K r/s3.72µs0.17µs0.25µs
100,000161K r/s6.19µs0.21µs0.25µs
250,00057.8K r/s17.3µs0.21µs0.25µs
500,00027.8K r/s36.0µs0.33µs0.38µs
1,000,00013.3K r/s75.1µs0.50µs0.54µs

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99
100786K r/s1.27 µs1.40 µs1.64 µs
1,0003.92M r/s255 ns235 ns292 ns
10,000836K r/s1.20 µs596 ns739 ns
20,000346K r/s2.89 µs785 ns966 ns
50,000160K r/s6.26 µs865 ns1.00 µs
100,00080K r/s12.5 µs944 ns1.10 µs
250,00025K r/s39.3 µs1.68 µs1.89 µs
500,00016K r/s61.7 µs1.25 µs1.39 µs
1,000,0007.6K r/s131 µs1.71 µs1.88 µs

Mixed Workload (Throughput)

80% matching + 20% non-matching, cache enabled, 1,000 warmup.

ARM64 (Apple M4)

RulesThroughputMeanP95P99
1001.93M r/s518 ns1.00 µs1.17 µs
1,0005.21M r/s192 ns250 ns292 ns
10,0004.18M r/s239 ns375 ns417 ns
20,0003.20M r/s313 ns542 ns625 ns
50,0001.88M r/s531 ns1.04 µs1.29 µs
100,0001.09M r/s919 ns1.96 µs2.46 µs
250,000500K r/s2.00 µs4.50 µs5.67 µs
500,000259K r/s3.86 µs8.79 µs11.0 µs
1,000,000131K r/s7.61 µs17.6 µs22.2 µs

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99
1001.58M r/s631 ns1.31 µs1.43 µs
1,0005.98M r/s167 ns218 ns246 ns
10,0003.48M r/s287 ns427 ns520 ns
20,0002.85M r/s351 ns537 ns626 ns
50,0001.55M r/s646 ns1.13 µs1.36 µs
100,000718K r/s1.39 µs2.62 µs3.16 µs
250,000524K r/s1.91 µs3.87 µs4.83 µs
500,000348K r/s2.87 µs6.33 µs7.88 µs
1,000,000192K r/s5.20 µs11.2 µs14.0 µs

LDAP ACI Performance

LDAP Access Control Instructions use a DnScopeIndex trie for O(depth) candidate selection, with dual LRU caches (check_access + authorize) in CachedAciPolicy. Cached evaluation is essentially O(1) — latency stays flat at 47–75 ns regardless of rule count. Uncached evaluation uses objectclass pre-filtering, userdn pre-filtering, and attribute partitioning to eliminate ~75% of candidates before per-ACI checks, scaling from 598 ns at 100 rules to 1.5 ms at 1M rules.

The synthetic workload matches real FreeIPA ACI distributions: 60% permission-based (GroupDn + ObjectClass filter + targetattr), 15% authenticated read (ObjectClass filter), 10% admin group, 5% anonymous read, 5% self-service, 5% narrow UserDn scope. Non-matching requests use valid DNs with mismatched objectclasses, exercising the full filter/bind/attr rejection pipeline rather than trivially failing at the DN trie.

Cached (LRU-warm)

10,000 matching requests after 1,000 warmup requests. The LRU cache dominates.

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99
10016.5M r/s60 ns66 ns76 ns
1,00014.3M r/s70 ns76 ns86 ns
10,00014.5M r/s69 ns75 ns86 ns
20,00014.4M r/s69 ns77 ns85 ns
50,00015.1M r/s66 ns74 ns81 ns
100,00015.0M r/s66 ns75 ns83 ns
250,00015.2M r/s66 ns71 ns81 ns
500,00016.0M r/s62 ns70 ns75 ns
1,000,00013.4M r/s75 ns83 ns93 ns

check_access (LRU-warm)

check_access() returns bool instead of AuthorizationResult, avoiding the PermissionSet allocation overhead.

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99
10021.1M r/s47 ns53 ns58 ns
1,00018.0M r/s55 ns61 ns65 ns
10,00018.5M r/s54 ns61 ns64 ns
20,00018.3M r/s55 ns61 ns65 ns
50,00019.7M r/s51 ns58 ns63 ns
100,00019.1M r/s52 ns61 ns67 ns
250,00019.5M r/s51 ns58 ns63 ns
500,00017.3M r/s58 ns66 ns69 ns
1,000,00016.5M r/s61 ns68 ns72 ns

Uncached

10,000 unique non-matching requests, 0 warmup. Every request exercises the full evaluation path (DN trie traversal, objectclass/userdn pre-filtering, scope matching, bind rule checks). Non-matching requests use valid DNs with mismatched objectclasses (“device”), so they pass the DN trie but are rejected by filter/bind/attr checks — measuring realistic rejection cost.

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99
1001.7M r/s598 ns734 ns833 ns
1,000667K r/s1.50 µs1.68 µs1.78 µs
10,000205K r/s4.89 µs5.53 µs9.37 µs
20,000105K r/s9.53 µs10.91 µs15.46 µs
50,00040K r/s25.2 µs28.4 µs32.6 µs
100,00015K r/s65.2 µs89.4 µs104 µs
250,0005K r/s221 µs280 µs299 µs
500,0002K r/s587 µs714 µs832 µs
1,000,0001K r/s1.50 ms1.86 ms1.98 ms

Mixed Workload (Throughput)

80% matching + 20% non-matching, cache enabled, 1,000 warmup.

x86_64 (Intel i7-12800H)

RulesThroughputMeanP95P99
10015.1M r/s66 ns73 ns85 ns
1,00014.1M r/s71 ns81 ns92 ns
10,00012.7M r/s79 ns90 ns99 ns
20,00011.3M r/s89 ns102 ns114 ns
50,00011.5M r/s87 ns102 ns168 ns
100,00012.7M r/s78 ns89 ns104 ns
250,00012.3M r/s81 ns90 ns154 ns
500,00012.1M r/s83 ns96 ns174 ns
1,000,00013.6M r/s74 ns86 ns157 ns

Windows SD Performance

Access check latency for Windows Security Descriptors using the check_access algorithm (DACL walk with deny-before-allow, SID matching, owner implicit rights). The round-robin adapter evaluates one SD per request, so latency reflects the per-SD access check cost.

Unlike HBAC/ABAC (which search across a rule set), a Windows access check walks a single SD’s DACL — typically 3–8 ACEs. Latency is therefore nearly constant regardless of how many SDs are loaded; the rule count affects only memory and load time.

ARM64 (Apple M4)

Check Access Latency

RulesMean (ns)P95 (ns)P99 (ns)Throughput (req/s)
10044838422.6M
1,00038424226.4M
10,00023428443.1M
100,000214212548.7M

Throughput (mixed matching/non-matching)

RulesMean (ns)P95 (ns)Throughput (req/s)
100598417.1M
1,000394225.4M
10,000234242.9M
100,000214247.9M

Uncached Latency (diverse non-matching)

RulesMean (ns)P95 (ns)Throughput (req/s)
100498320.5M
1,000364227.5M
10,000224245.3M
100,000214247.5M

Policy Build Time and Memory

RulesBuild (ms)Memory (KB)Per rule (B)
1000.0413.3136
1,0000.20132.8136
10,0001.091328.1136
100,0009.4013281.3136

Cross-engine comparison (check_access, 10K rules)

EngineMean (ns)P95 (ns)Throughput (req/s)
win-sd234243.1M
posix-acl294234.4M

Windows SD access checks are slightly faster than POSIX ACL checks despite the more complex permission model (32-bit masks vs 3-bit rwx, explicit deny ACEs, SID matching vs string comparison). Both are in the same performance class at sub-50ns latency.


Policy Build Time

One-time cost when a policy is loaded or reloaded. Includes Bloom filter construction, decision tree compilation, deny-rule index building, and composite index creation for HBAC, or compiled evaluator, bitmap deny index, and composite index for ABAC, or DN trie index construction and LRU cache initialization for LDAP ACI, or simple Vec loading for Windows SD and POSIX ACL.

HBAC Build Time

ARM64 (Apple M4)

RulesBuild TimePer 1K Rules
1000.01 ms0.12 ms
1,0000.09 ms0.09 ms
10,0000.71 ms0.07 ms
20,0001.52 ms0.08 ms
50,0003.81 ms0.08 ms
100,0008.02 ms0.08 ms
250,00020.8 ms0.08 ms
500,00040.9 ms0.08 ms
1,000,00087.6 ms0.09 ms

x86_64 (Intel i7-12800H)

RulesBuild TimePer 1K Rules
1000.04 ms0.38 ms
1,0000.13 ms0.13 ms
10,0001.57 ms0.16 ms
20,0005.57 ms0.28 ms
50,00019.4 ms0.39 ms
100,00041.7 ms0.42 ms
250,000106 ms0.43 ms
500,000202 ms0.40 ms
1,000,000424 ms0.42 ms

ABAC Build Time

ARM64 (Apple M4)

RulesBuild TimePer 1K Rules
1000.15 ms1.50 ms
1,0000.83 ms0.83 ms
10,0006.19 ms0.62 ms
20,00013.4 ms0.67 ms
50,00033.4 ms0.67 ms
100,00060.7 ms0.61 ms
250,000167 ms0.67 ms
500,000354 ms0.71 ms
1,000,000798 ms0.80 ms

x86_64 (Intel i7-12800H)

RulesBuild TimePer 1K Rules
1000.11 ms1.10 ms
1,0000.47 ms0.47 ms
10,0008.79 ms0.88 ms
20,00019.8 ms0.99 ms
50,00056.6 ms1.13 ms
100,000122 ms1.22 ms
250,000317 ms1.27 ms
500,000611 ms1.22 ms
1,000,0001,175 ms1.18 ms

LDAP ACI Build Time

Build time includes DN trie construction, objectclass/userdn pre-filtering index building, and attribute partitioning.

x86_64 (Intel i7-12800H)

RulesBuild TimePer 1K Rules
1000.26 ms2.58 ms
1,0001.76 ms1.76 ms
10,00020.1 ms2.01 ms
20,00038.3 ms1.92 ms
50,000104 ms2.07 ms
100,000199 ms1.99 ms
250,000530 ms2.12 ms
500,0001.04 s2.09 ms
1,000,0002.1 s2.06 ms

Memory

HBAC Memory Usage

ARM64 (Apple M4)

RulesTotalPer-Rule
10034 KB0.35 KB
1,000334 KB0.34 KB
10,0003.3 MB0.34 KB
20,0006.6 MB0.34 KB
50,00016.5 MB0.34 KB
100,00033 MB0.34 KB
250,00082.5 MB0.34 KB
500,000165 MB0.34 KB
1,000,000330 MB0.34 KB

x86_64 (Intel i7-12800H)

RulesTotalPer-Rule
10034 KB0.34 KB
1,000334 KB0.34 KB
10,0003.2 MB0.33 KB
20,0006.4 MB0.33 KB
50,00016.1 MB0.33 KB
100,00032.3 MB0.33 KB
250,00080.7 MB0.33 KB
500,000161.5 MB0.33 KB
1,000,000323.1 MB0.33 KB

ABAC Memory Usage

ARM64 (Apple M4)

RulesTotalPer-Rule
1007.8 KB0.080 KB
1,00078 KB0.080 KB
10,000781 KB0.080 KB
20,0001.6 MB0.080 KB
50,0003.9 MB0.080 KB
100,0007.8 MB0.080 KB
250,00019.5 MB0.080 KB
500,00039 MB0.080 KB
1,000,00078 MB0.080 KB

x86_64 (Intel i7-12800H)

RulesTotalPer-Rule
1007.8 KB0.078 KB
1,00078 KB0.078 KB
10,000781 KB0.078 KB
20,0001.5 MB0.078 KB
50,0003.8 MB0.078 KB
100,0007.6 MB0.078 KB
250,00019 MB0.078 KB
500,00038 MB0.078 KB
1,000,00076 MB0.078 KB

ABAC uses 4.2× less memory than HBAC due to the compiled evaluator avoiding per-rule storage overhead.

Per-rule cost is the average for the sssd-prod distribution. Category=all rules are smaller (no entity strings); specific rules with many entities are larger.

Index Overhead

  • LRU cache: configurable (default 1,024 entries) × ~64 bytes = ~64 KB
  • Bloom filters: ~1 KB per 1,000 rules
  • Decision trees: ~0.05 KB per deny rule
  • Deny-rule index: ~1 KB per deny rule (bitmap indexes over user/host/service dimensions)
  • Total: <1% of rule storage at 10K+ rules

Performance Crossover

HBAC vs ABAC vs LDAP ACI — Mixed Workload (x86_64)

RulesHBACABACLDAP ACIFastest
10015.6M r/s1.58M r/s15.1M r/sHBAC
1,00014.8M r/s5.98M r/s14.1M r/sHBAC
10,00011.3M r/s3.48M r/s12.7M r/sLDAP ACI
20,00011.7M r/s2.85M r/s11.3M r/sHBAC
50,00011.0M r/s1.55M r/s11.5M r/sLDAP ACI
100,0009.58M r/s718K r/s12.7M r/sLDAP ACI
250,0009.31M r/s524K r/s12.3M r/sLDAP ACI
500,0009.87M r/s348K r/s12.1M r/sLDAP ACI
1,000,0009.17M r/s192K r/s13.6M r/sLDAP ACI

LDAP ACI is the fastest engine at 10K+ rules in the mixed workload, with throughput of 11–15M r/s thanks to its dual LRU cache and DnScopeIndex trie. HBAC is slightly faster at small scales (100–1K rules) due to lower per-request overhead, while ABAC lags further due to rule traversal even on cache hits. The mixed workload uses realistic non-matching requests that exercise objectclass/userdn pre-filtering and attribute rejection, so the 20% non-matching portion has slightly higher latency than trivial rejections.

Cached Workloads (typical SSSD deployments)

ARM64 (Apple M4):

  • At 100 rules: ABAC is 2.7× faster (7.41M vs 2.78M req/s)
  • At 100K rules: ABAC is 3.3× faster (830K vs 252K req/s)

x86_64 (Intel i7-12800H):

  • At 100 rules: HBAC is 7.2× faster than ABAC (16.0M vs 2.22M req/s)
  • At 1K rules: HBAC fastest (14.7M r/s) > LDAP ACI (14.3M) > ABAC (5.69M)
  • At 100K rules: LDAP ACI (15.0M) > HBAC (11.5M) > ABAC (949K r/s)
  • At 1M rules: LDAP ACI (13.4M) > HBAC (11.3M) > ABAC (143K r/s)

Uncached Workloads

ARM64 (Apple M4):

  • At 10K rules: HBAC is 6.6× faster uncached (1.18M vs 179K req/s)
  • At 100K rules: HBAC is 8.6× faster uncached (108K vs 12.6K req/s)

x86_64 (Intel i7-12800H):

  • At 1K rules: HBAC (6.50M) > ABAC (3.92M) > LDAP ACI (667K r/s)
  • At 10K rules: HBAC (2.50M) > ABAC (836K) > LDAP ACI (205K r/s)
  • At 100K rules: HBAC (249K) > ABAC (80K) > LDAP ACI (15K r/s)
  • At 1M rules: HBAC (23K) > ABAC (7.6K) > LDAP ACI (1K r/s)

HBAC and ABAC are faster than LDAP ACI for uncached workloads. LDAP ACI uncached numbers reflect a realistic workload where non-matching requests use valid DNs that pass the DN trie but are rejected by objectclass pre-filtering, userdn pre-filtering, and attribute checks — measuring the true cost of filter/bind rejection rather than trivial DN misses. HBAC and ABAC use Bloom filters and decision trees for faster uncached rejection.

Recommendation: For cached workloads, LDAP ACI is the fastest engine. For uncached workloads, HBAC is dramatically faster. Use ABAC when you need N-dimensional attribute-based rules or 4.2× less memory. Choose the engine that matches your access control model.

Optimization Layers

ABAC uses a 6-layer optimization pipeline:

  1. Constant-result fast path – single outcome (~15 ns)
  2. Bitmap deny index – u64 bitmask intersection when universal allow exists
  3. LRU cache – memoization (sub-microsecond, for non-indexed fallback)
  4. AHash – 2-3× faster than SipHash for non-cryptographic use
  5. Compiled evaluator – pre-extracted attributes + array indexing
  6. Composite index – candidate selection fallback (O(log n))
  7. Deny-only indexing – skip allow rules when universal allow exists

Key optimization: The bitmap deny index (Layer 1) uses pre-computed BitPos structs and per-dimension inverted indexes with u64 bitmask AND operations. When a universal allow rule exists, evaluation bypasses the cache entirely and resolves via bitmap intersection in O(deny_rules/64) per dimension. This produces excellent throughput at 1K–10K rules (3–5M r/s) where the bitmaps fit in L1 cache, with graceful degradation at larger scales.

Layer 4 (Compiled evaluator) pre-extracts request attributes once (3 HashMap lookups) into a stack-allocated array, then uses array indexing for all rule checks. This serves as the fallback when no universal allow rule exists.

Temporal Rule Support

Both HBAC and ABAC support time-based rules via TemporalHbacRule and TemporalAbacRule. Temporal rules are evaluated separately and incur minimal overhead when no temporal rules are active (~5-10ns for the empty-check).

When temporal rules are present:

  • Active temporal rules (within time window) are evaluated alongside regular rules
  • Inactive rules (outside time window) are skipped at evaluation time
  • No additional caching overhead
  • Build time increases by ~1-2ms per 1,000 temporal rules

See the Temporal Rules guide for usage examples.


JIT Compilation

Optional feature flag (--features jit). Compiles per-rule evaluation to native code.

WorkloadEffect
CachedNo measurable difference (±3-6% noise). The LRU cache already provides sub-microsecond lookups.
Uncached~8-12% throughput improvement at all scales. Every request exercises the full evaluation path where native code pays off.
BuildNo consistent overhead (±8% noise).

JIT is worth enabling when uncached or low-hit-rate workloads dominate. For typical SSSD deployments with high cache hit rates, it provides no benefit.


Optimization Stack

Evaluations pass through these layers in order. Each layer is skipped if the previous one already produced a result.

Layer 0 — Constant-Result Fast Path

When the decision tree collapses to a single outcome (e.g., a universal deny rule exists, or a universal allow with no deny rules), the result is stored and returned immediately. Bypasses all other layers. ~15 ns per evaluation.

Layer 1 — Bitmap Deny Index

When a universal allow rule exists, the deny index short-circuits evaluation using bitmap intersection. Pre-computed BitPos structs map each deny rule to a (word, mask) pair. Per-dimension inverted indexes map attribute values to their rule bitmasks. Evaluation ANDs dimension bitmasks together; any surviving bit means a deny match. Cost: O(deny_rules/64) per dimension. Bypasses the LRU cache entirely — no RequestKey hashing overhead.

Layer 2 — LRU Memoization Cache

Configurable-size cache (default 1,024 entries) keyed by a u64 hash of (user, host, service, groups). Order-independent group hashing via wrapping addition. Zero heap allocations per lookup. Sub-100ns hit latency. Used when the bitmap deny index is not applicable.

Layer 3 — Bloom Filter Pre-screening

Fast negative check before full evaluation. 1% false positive rate. Effective for non-matching requests when no category=all rules exist. In SSSD deployments where category=all is common, the Bloom filter rarely rejects.

Layer 4 — Decision Tree

Rules are compiled into a decision tree at load time. When a universal allow exists alongside specific deny rules, the tree stores only the deny rules — reducing evaluation from O(n) to O(d) where d is the deny rule count.

Layer 5 — Composite Indexing (Fallback)

Indexed user+host lookup for candidate selection. Used when the decision tree is not available. O(1) for category=all, O(log n) for specific entities.

Layer 6 — JIT Compilation (optional)

Requires the jit feature flag. Compiles per-rule evaluation to native code. ~10% throughput gain on uncached workloads; no gain on cached workloads.


Deployment Guidance

Typical SSSD (100–1,000 rules)

Most FreeIPA environments fall here.

HBAC (x86_64): 62–66 ns cached latency (evaluate), 52–60 ns (check_access), 15.1–16.0M req/s throughput, 34–334 KB memory, <0.13 ms build time.

ABAC (x86_64): 176–450 ns cached latency, 2.22–5.69M req/s throughput, 7.8–78 KB memory, <0.47 ms build time.

LDAP ACI (x86_64): 60–70 ns cached latency, 47–55 ns (check_access), 14.3–16.5M req/s, <1.76 ms build time.

Recommendation: LDAP ACI and HBAC deliver near-identical cached latency at this scale. Use ABAC when you need N-dimensional flexibility or 4.2× less memory. Choose the engine that matches your access control model.

Large Enterprise (1K–10K rules)

HBAC (x86_64): 66–72 ns cached latency, 13.9–15.1M req/s, 334 KB–3.2 MB memory, 0.13–1.57 ms build time.

ABAC (x86_64): 176–252 ns cached latency, 3.97–5.69M req/s, 78–781 KB memory, 0.47–8.79 ms build time.

LDAP ACI (x86_64): 69–70 ns cached latency, 54–55 ns (check_access), 14.3–14.5M req/s, 1.76–20.1 ms build time.

Recommendation: HBAC and LDAP ACI are close at this scale (14.3M vs 14.8M r/s at 1K rules). Both use LRU caching for flat latency. Use ABAC for N-dimensional flexibility or lower memory (4.2× less).

Very Large (10K–100K rules)

HBAC (x86_64): 72–87 ns cached latency, 11.5–13.9M req/s, 3.2–32.3 MB memory, 1.57–41.7 ms build time.

ABAC (x86_64): 252 ns–1.05 µs cached latency, 949K–3.97M req/s, 781 KB–7.6 MB memory, 8.79–122 ms build time.

LDAP ACI (x86_64): 66–69 ns cached latency, 51–52 ns (check_access), 14.4–15.1M req/s, 20.1–199 ms build time.

LDAP ACI is 1.3× faster than HBAC at 100K rules (15.0M vs 11.5M r/s). ABAC uses 4.2× less memory than HBAC.

Recommendation: LDAP ACI and HBAC are both excellent at this scale. Choose ABAC if memory is a constraint (7.6 MB vs 32.3 MB at 100K rules) or you need N-dimensional rules.

Extreme Scale (100K–1M rules)

HBAC (x86_64): 87–88 ns cached latency, 11.3–11.5M req/s, 32.3–323 MB memory, 41.7–424 ms build time.

ABAC (x86_64): 1.05–7.01 µs cached latency, 143K–949K req/s, 7.6–76 MB memory, 122 ms–1.18 s build time.

LDAP ACI (x86_64): 62–75 ns cached latency, 51–61 ns (check_access), 13.4–16.0M req/s, 199 ms–2.1 s build time.

At 1M rules, LDAP ACI is 1.2× faster than HBAC (13.4M vs 11.3M r/s) and 94× faster than ABAC (13.4M vs 143K r/s).

Recommendation: LDAP ACI delivers the best cached throughput at extreme scale. HBAC is close behind. Choose ABAC if memory is the primary constraint (76 MB vs 323 MB at 1M rules) or you need N-dimensional rules.

Cache Tuning

The default 1,024-entry LRU is optimal for most workloads.

  • Higher hit rate: 2,048 or 4,096 entries (+64-192 KB memory)
  • Memory constrained: 512 entries (~85% hit rate)
  • Cold-start sensitive: pre-warm with common requests after policy load
  • Diverse patterns: increase cache size if hit rate drops below 80%

Benchmark Methodology

Scenarios

ScenarioRequestsWarmupDistribution
single-latency10,0001,000All matching (pool: 200)
check-access-latency10,0001,000All matching (pool: 200, bool)
uncached-latency10,0000All non-matching (pool: 5,000)
throughput10,0001,00080/20 mixed (pool: 500)
build-time10Matching

All evaluation scenarios pre-generate a request pool before measurement. Requests cycle through the pool during warmup and measurement, ensuring that only evaluation time is measured — not request object construction. check-access-latency uses check_access() (returns bool) instead of evaluate() (returns full result with matched rule lists).

Fixtures

Synthetic fixtures use the sssd-prod distribution, which models real SSSD deployments: 15% user category=all, 30% host category=all, 40% service category=all, 5% deny rules, 2% disabled rules. Universal deny rules (all three dimensions category=all with type=deny) are excluded — real deployments never create these, and they would collapse the decision tree to a constant.

LDAP ACI fixtures use the ldap-aci distribution, which models real 389-ds deployments with varied DN scopes, bind rules (identity + environment), target filters, attribute sets, and grant/deny ACIs.

Measurement

  • Seeded RNG: StdRng::seed_from_u64(42) for reproducibility
  • Pre-generated pools: requests are created before timing begins
  • Single-threaded: sequential request processing
  • Throughput: derived from the sum of per-request evaluate() latencies
  • Timing: only the evaluate()/check_access() call is measured

Running Benchmarks

Individual fixture generation and benchmarking:

# Generate fixtures
cargo run --release -p perf-testing -- \
  generate -b hbac -c 10000 -o hbac_10000_baseline --fixtures-dir fixtures

# Run all scenarios for HBAC
cargo run --release -p perf-testing bench -b hbac -f hbac_10000_baseline --cache

# Run all scenarios for ABAC
cargo run --release -p perf-testing bench -b abac -f abac_10000_baseline --cache

# Run all scenarios for LDAP ACI
cargo run --release -p perf-testing bench -b ldap_aci -f ldap_aci_10000_baseline --cache

# Run specific scenarios
cargo run --release -p perf-testing -- \
  list  # List available fixtures and scenarios

Full benchmark suite across all scales:

# HBAC benchmarks
scripts/bench.sh run --no-jit --rules 100,1000,10000,20000,50000,100000,250000,500000,1000000

# ABAC benchmarks
scripts/bench.sh run --no-jit --bac-type abac --rules 100,1000,10000,20000,50000,100000,250000,500000,1000000

# LDAP ACI benchmarks
scripts/bench.sh run --no-jit --bac-type ldap_aci --rules 100,1000,10000,20000,50000,100000,250000,500000,1000000

Criterion Benchmarks

For statistically rigorous measurements with confidence intervals:

cd crates/perf-testing
cargo bench --bench hbac_benchmarks
open target/criterion/report/index.html

Python Binding Benchmarks

For the Python binding performance results below:

source .venv/bin/activate
python python/collect_results.py --warmup 3 --measure 5

Python Binding Performance

Measured on x86_64 — Intel Core i7-12800H (Linux 6.15.8, Python 3.15). Same fixtures and request-generation logic as the Rust benchmarks. Policy is built once per fixture and shared across all eval scenarios.

Two build/eval strategies are benchmarked:

  • evaluate+builder: constructs rules via PyO3 builder pattern, evaluates with evaluate() (returns full Decision/HbacEvaluationResult)
  • check_access+json (HBAC only): loads rules via load_rules_json(), evaluates with check_access() (returns bool, avoids Vec<String> allocation per call)

For ABAC, load_rules_json() provides faster build times (no Python object construction) with identical evaluation speed — both paths produce the same internal rules.

Python — HBAC (evaluate+builder)

Cached (LRU-warm, matching requests)

RulesThroughputMeanP95P99
1006.43M r/s155 ns162 ns167 ns
1,0006.39M r/s157 ns161 ns165 ns
10,0006.26M r/s160 ns167 ns168 ns
20,0006.53M r/s153 ns161 ns165 ns
50,0006.46M r/s155 ns159 ns160 ns
100,0006.53M r/s153 ns156 ns163 ns
250,0006.38M r/s157 ns166 ns170 ns
500,0006.38M r/s157 ns161 ns164 ns
1,000,0006.21M r/s161 ns168 ns169 ns

Uncached (~80% cache misses, non-matching)

RulesThroughputMeanP95P99
1005.85M r/s171 ns183 ns186 ns
1,0004.39M r/s228 ns239 ns240 ns
10,0004.69M r/s213 ns227 ns233 ns
20,0004.18M r/s239 ns243 ns251 ns
50,0004.15M r/s241 ns246 ns249 ns
100,0003.65M r/s274 ns284 ns289 ns
250,0002.88M r/s347 ns366 ns371 ns
500,0002.02M r/s496 ns507 ns519 ns
1,000,0001.37M r/s731 ns749 ns754 ns

Mixed Workload (80% matching / 20% non-matching)

RulesThroughputMean
1006.47M r/s154 ns
1,0006.18M r/s162 ns
10,0006.46M r/s155 ns
20,0006.35M r/s157 ns
50,0006.32M r/s158 ns
100,0006.39M r/s156 ns
250,0006.32M r/s158 ns
500,0006.23M r/s161 ns
1,000,0006.01M r/s166 ns

Policy Build Time

RulesRule objects (py)load_rules() (rs)Total
1000.7 ms0.0 ms0.7 ms
1,0002.2 ms0.1 ms2.3 ms
10,00020.1 ms1.6 ms21.7 ms
20,00036.8 ms3.0 ms39.8 ms
50,00099.3 ms8.1 ms107.4 ms
100,000186.8 ms15.0 ms201.8 ms
250,000475.3 ms36.3 ms511.5 ms
500,000981.2 ms70.0 ms1,051 ms
1,000,0001,938 ms142.4 ms2,080 ms

Python — HBAC (check_access+json)

The optimized path: load_rules_json() skips Python rule-object allocation entirely, and check_access() returns a bool instead of allocating HbacEvaluationResult (3×Vec<String>).

Cached (LRU-warm, matching requests)

RulesThroughputMeanP95P99
1007.93M r/s126 ns131 ns134 ns
1,0008.25M r/s121 ns126 ns127 ns
10,0008.25M r/s121 ns126 ns128 ns
20,0008.15M r/s123 ns126 ns128 ns
50,0008.16M r/s123 ns126 ns127 ns
100,0008.14M r/s123 ns127 ns131 ns
250,0007.71M r/s130 ns138 ns159 ns
500,0007.68M r/s130 ns136 ns140 ns
1,000,0008.15M r/s123 ns127 ns130 ns

Uncached (~80% cache misses, non-matching)

RulesThroughputMeanP95P99
1006.46M r/s155 ns165 ns175 ns
1,0005.41M r/s185 ns203 ns207 ns
10,0005.53M r/s181 ns189 ns195 ns
20,0005.25M r/s191 ns196 ns205 ns
50,0004.87M r/s205 ns211 ns212 ns
100,0004.10M r/s244 ns258 ns261 ns
250,0003.07M r/s326 ns349 ns360 ns
500,0002.21M r/s452 ns471 ns480 ns
1,000,0001.35M r/s739 ns820 ns859 ns

Mixed Workload (80% matching / 20% non-matching)

RulesThroughputMean
1007.54M r/s133 ns
1,0007.75M r/s129 ns
10,0008.09M r/s124 ns
20,0008.10M r/s123 ns
50,0007.93M r/s126 ns
100,0007.77M r/s129 ns
250,0007.46M r/s134 ns
500,0007.82M r/s128 ns
1,000,0007.62M r/s131 ns

Policy Build Time (JSON)

Rulesload_rules_json() (rs)Speedup vs builder
1000.6 ms1.2×
1,0001.2 ms1.9×
10,0006.9 ms3.1×
20,00012.8 ms3.1×
50,00032.0 ms3.4×
100,00063.2 ms3.2×
250,000160.6 ms3.2×
500,000313.9 ms3.3×
1,000,000617.4 ms3.4×

Python — ABAC (evaluate+builder)

Cached (LRU-warm, matching requests)

RulesThroughputMeanP95P99
1001.66M r/s603 ns621 ns626 ns
1,0003.12M r/s320 ns330 ns334 ns
10,0002.44M r/s410 ns422 ns426 ns
20,0002.22M r/s451 ns467 ns478 ns
50,0001.54M r/s649 ns683 ns723 ns
100,0001.02M r/s982 ns1.04 µs1.14 µs
250,000524K r/s1.91 µs2.05 µs2.07 µs
500,000278K r/s3.60 µs3.69 µs3.78 µs
1,000,000159K r/s6.29 µs7.13 µs7.18 µs

ABAC evaluation scales inversely with rule count — no FFI floor like the prior (incorrect) ARM64 measurements suggested. At 1K rules the compiled evaluator delivers 3.1M r/s; at 1M rules the larger index scan drops to 159K.

Uncached (~80% cache misses, non-matching)

RulesThroughputMeanP95P99
100649K r/s1.54 µs1.66 µs1.69 µs
1,0003.41M r/s293 ns310 ns316 ns
10,0003.35M r/s299 ns316 ns322 ns
20,0003.46M r/s289 ns298 ns304 ns
50,0003.32M r/s301 ns310 ns316 ns
100,0003.13M r/s319 ns328 ns336 ns
250,0002.56M r/s390 ns404 ns420 ns
500,0001.94M r/s515 ns534 ns562 ns
1,000,0001.55M r/s645 ns671 ns691 ns

Uncached is faster than cached at 1K+ rules because the Bloom filter rejects non-matching requests before rule iteration.

Mixed Workload (80% matching / 20% non-matching)

RulesThroughputMean
1001.12M r/s896 ns
1,0003.17M r/s315 ns
10,0002.57M r/s389 ns
20,0002.28M r/s439 ns
50,0001.66M r/s603 ns
100,0001.20M r/s835 ns
250,000633K r/s1.58 µs
500,000337K r/s2.97 µs
1,000,000198K r/s5.06 µs

Policy Build Time

RulesRule objects (py)load_rules() (rs)Total
1000.7 ms0.1 ms0.7 ms
1,0003.3 ms0.7 ms3.9 ms
10,00030.5 ms7.5 ms38.0 ms
20,00059.8 ms15.0 ms74.8 ms
50,000148.1 ms34.0 ms182.1 ms
100,000304.1 ms68.1 ms372.2 ms
250,000788.5 ms183.2 ms971.7 ms
500,0001,564 ms370.3 ms1,935 ms
1,000,0003,123 ms769.5 ms3,892 ms

Using load_rules_json() instead of the builder pattern is 2.6–3.1× faster for build time (same eval performance):

RulesBuilder TotalJSON TotalSpeedup
1,0003.9 ms1.5 ms2.6×
10,00038.0 ms13.3 ms2.9×
100,000372.2 ms121.3 ms3.1×
1,000,0003,892 ms1,354 ms2.9×

Python vs Rust — Summary (x86_64)

MetricRust (x86_64)Python (x86_64)Ratio
HBAC evaluate (cached, 1K)66 ns157 nsRust 2.4× faster
HBAC check_access (cached, 1K)60 ns121 nsRust 2.0× faster
HBAC evaluate (cached, 100K)87 ns153 nsRust 1.8× faster
HBAC check_access (cached, 100K)76 ns123 nsRust 1.6× faster
HBAC throughput (1K)14.8M r/s6.18M r/sRust 2.4× faster
HBAC build (10K, builder)1.57 ms21.7 ms~14× slower
HBAC build (10K, json)1.57 ms6.9 ms~4.4× slower
ABAC eval (cached, 1K)176 ns320 nsRust 1.8× faster
ABAC eval (cached, 100K)1.05 µs982 ns~equal
ABAC throughput (10K)3.48M r/s2.57M r/sRust 1.4× faster
ABAC build (10K, builder)8.79 ms38.0 ms~4.3× slower
ABAC build (10K, json)8.79 ms13.3 ms~1.5× slower

Key observations:

  • HBAC evaluation: With pre-generated request pools (fixing the old benchmark methodology that counted request allocation), native Rust is 1.6–2.4× faster than Python. The check_access() path (returning bool instead of HbacEvaluationResult) is the fastest option in both Rust and Python. Python’s #[pyclass(frozen)] optimization still delivers impressive throughput (8.25M r/s at 1K rules).

  • ABAC evaluation: At small scales (1K), Rust is ~1.8× faster. At 100K rules, Python is on par — the compiled evaluator dominates and the PyO3 call overhead is negligible relative to rule evaluation time.

  • Build time: The load_rules_json() path eliminates Python rule-object construction entirely, achieving build times within 1.5–4.4× of native Rust. The builder path is 4–14× slower due to per-rule FFI round-trips.

  • Use case: Python bindings deliver near-native evaluation performance and are suitable for production data-plane use. Use load_rules_json() and check_access() (HBAC) for maximum throughput.