Skip to main content

UI Performance Benchmark Results

Audience: Contributors deciding which UI performance changes to keep.

Branch: prebuild/feat/audit-log-configurable-backend

Recorded: June 20, 2026

This page captures the benchmark runs from the audit-service and UI performance work. Use the clean 300-user before/after run as the primary decision data. The exploratory runs are useful directionally, but they compare different runtime shapes.


Summary

Change setBest evidenceResult
Short-TTL server response cacheClean 300-user local production runRPS 261.6 -> 2355.4, p95 2405.6 ms -> 354.7 ms
MongoDB connection single-flight, pool tuning, hot indexesClean 300-user local production runChat list p95 1660.7 ms -> 405.7 ms
JWT/API error log reductionClean 300-user local production runMissing-chat error path p95 767.6 ms -> 217.5 ms
Audit-service offloadAudit/RBAC benchmarkRemoves UI storage ownership; audit writes become non-blocking
OpenFGA BatchCheckAudit/RBAC benchmarkadmin-tab-gates p50 200 ms -> 46 ms at 300 users

Primary recommendation:

  • Keep the route cache, MongoDB connection/index changes, and log reduction.
  • Keep audit storage outside the UI. The UI should emit audit events and tolerate audit-service outages.
  • Re-run the clean benchmark in the final caipe-ui-prod container before release if container parity is required.

Clean 300-User UI Benchmark

This is the most comparable before/after run for the latest UI optimizations.

Runtime

ItemValue
UI serverLocal production next start on http://localhost:3005
DependenciesExisting Docker Compose services from docker-compose.dev.yaml
AuthKeycloak caipe-platform service-account bearer token
AuditAUDIT_LOG_BACKEND=off
Users300 virtual HTTP users
Duration30 seconds
Failure count0 before, 0 after

Environment notes:

  • MONGODB_URI=mongodb://admin:changeme@localhost:27017/caipe?authSource=admin
  • OPENFGA_HTTP=http://127.0.0.1:18080
  • KEYCLOAK_URL=http://127.0.0.1:7080
  • RAG_SERVER_URL=http://127.0.0.1:9446
  • AGENTGATEWAY_ADMIN_CONFIG_URL=http://127.0.0.1:15000/config
  • DYNAMIC_AGENTS_URL=http://127.0.0.1:8100

Workload

EndpointWeightExpected status
/api/chat/conversations?page=1&page_size=20&client_type=webui5200
/api/chat/conversations/0000000000000000000000001400 or 404
/api/auth/session4200
/api/dynamic-agents/available3200
/api/admin/stats2200
/api/admin/platform-config1403
/api/platform/health1200 or 503

Important caveats:

  • /api/rbac/admin-tab-gates is cookie-session only, so it was not load-tested in this bearer-token harness.
  • /api/platform/health returned 503 because some local host-mapped dependency checks failed. The latency and cache behavior are still useful.
  • /api/admin/platform-config returned 403 for the service account. This measures the forbidden path, not the authorized success path.
  • The synthetic conversation id is intentionally malformed and exercises the error path.

Overall result

MetricBeforeAfterChange
Total requests8,17670,904+767%
RPS261.62355.4+800%
Avg latency1129.7 ms127.0 ms-88.8%
p501269.5 ms34.6 ms-97.3%
p902175.5 ms306.9 ms-85.9%
p952405.6 ms354.7 ms-85.3%
p992750.8 ms463.4 ms-83.2%
Max2944.3 ms4644.9 msHigher outlier during heavier throughput

Endpoint result

EndpointBefore p50Before p95After p50After p95Notes
Chat list1270.2 ms1660.7 ms277.3 ms405.7 msMongo pool/index changes helped the hot read path
Missing chat424.2 ms767.6 ms142.4 ms217.5 msLess 4xx stack logging on the error path
Auth session3.5 ms17.2 ms22.3 ms45.6 msStill sub-50 ms p95 while serving 9x RPS
Dynamic agents available2154.3 ms2752.4 ms22.7 ms53.8 msShort-TTL response cache
Admin stats1731.0 ms2210.2 ms22.2 ms48.0 msShort-TTL response cache
Platform config forbidden439.2 ms721.0 ms85.8 ms172.7 msForbidden path only
Platform health1679.8 ms2166.5 ms24.1 ms373.6 msShort-TTL cache, local setup returned 503

Cache behavior

EndpointHitsMissesShared in-flightHit rate
Dynamic agents available12,139334297.2%
Admin stats8,266211798.6%
Platform health3,693644189.2%

shared means multiple requests joined one in-flight upstream fetch instead of starting duplicate work.

Tunables added

Env varDefaultPurpose
PLATFORM_HEALTH_CACHE_TTL_MS5000Cache health responses, including 503
DYNAMIC_AGENTS_AVAILABLE_CACHE_TTL_MS10000Cache dynamic agent discovery
ADMIN_STATS_CACHE_TTL_MS15000Cache admin dashboard stats
PLATFORM_CONFIG_CACHE_TTL_MS10000Cache platform config GETs; PATCH clears cache
ADMIN_TAB_GATES_CACHE_TTL_MS10000Cache admin tab gates per user
MONGODB_MAX_POOL_SIZE50Increase UI MongoDB pool headroom
MONGODB_MIN_POOL_SIZE2Keep warm MongoDB connections
AUTH_JWT_DEBUGunsetOpt in to successful JWT validation logs
API_ERROR_LOG_4XXunsetOpt in to 4xx stack logging

Exploratory 300-User Branch Comparison

This run compared the existing Docker container on port 3000 with the branch running locally on port 3005. Treat it as directional because the runtime shape was not identical.

Overall

MetricBefore containerBranch local production
Users300300
Total requests8,76110,378
Failures4830
RPS186.1220.9
Avg latency295.3 ms59.4 ms
p50108.2 ms9.9 ms
p90843.0 ms51.9 ms
p951183.6 ms101.6 ms
p991758.5 ms1735.2 ms
Max1975.4 ms2440.5 ms

The 483 baseline failures were from the synthetic fake-conversation status handling in the harness.

Endpoint p95

EndpointBefore container p95Branch p95
Chat list1102.2 ms105.9 ms
Fake chat get445.1 ms94.1 ms
Auth session79.0 ms15.9 ms
Admin tab gates182.9 ms55.5 ms
Admin stats1664.3 ms160.7 ms
Dynamic agents available1751.5 ms144.4 ms
Platform config863.8 ms79.7 ms
Platform health1672.6 ms163.0 ms

Route and API Smoke Benchmark

This smaller run used 10 browser route iterations and 40 API samples. It compared the Docker baseline with the local production branch, so use it as smoke-test data.

Client route transitions stayed in the same range, roughly 25-34 ms. The larger gain was on API latency.

EndpointBefore p50Before p95Branch p50Branch p95
/api/rbac/admin-tab-gates18.3 ms30.3 ms9.1 ms20.2 ms
/api/admin/stats16.4 ms29.1 ms12.0 ms21.8 ms
/api/dynamic-agents/available24.3 ms41.3 ms15.2 ms18.5 ms
/api/admin/platform-config13.6 ms24.0 ms6.5 ms10.5 ms
/api/auth/session4.5 ms7.2 ms2.7 ms4.3 ms

Audit and RBAC Benchmark Summary

Full audit/RBAC data lives in Audit Log Backend Performance.

Admin tab gates, 20 users

Strategyp50p75p95p99
MongoDB audit + sequential OpenFGA170 ms190 ms390 ms760 ms
Local audit-service + Promise.all130 ms150 ms230 ms350 ms
S3 audit-service + Promise.all130 ms140 ms170 ms220 ms

Admin tab gates, 300 users

Strategyp50p75p95p99
18 parallel RPCs with Promise.all200 ms310 ms800 ms960 ms
OpenFGA BatchCheck single RPC46 ms93 ms450 ms780 ms

300 users, all endpoints

EndpointS3 + Promise.all p95S3 + BatchCheck p95
/api/auth/session48 ms53 ms
/api/chat/conversations190 ms400 ms
/api/admin/stats400 ms650 ms
/api/dynamic-agents/available400 ms640 ms
/api/rbac/admin-tab-gates800 ms450 ms

Interpretation:

  • BatchCheck is a clear win for the admin gate critical path.
  • The non-RBAC endpoints became the next bottleneck at 300 users.
  • The later short-TTL route cache directly targets those non-RBAC endpoints.

Commit Decision Notes

ChangeKeep?Why
Audit-service owns local and S3 storageYesKeeps UI storage-free and makes audit non-critical-path
Drop audit events when service is downYesMatches product behavior: warn in health, do not break navigation
OpenFGA BatchCheckYesLargest RBAC-specific improvement under 300 users
Route response cacheYesLargest overall 300-user UI improvement
MongoDB pool/index/single-flight changesYesImproves chat list and reduces startup/index churn
JWT success log opt-inYesRemoves high-volume success logs under load
4xx stack log opt-inYesKeeps expected client errors from becoming a throughput problem

Release validation still needed:

  • Re-run the clean 300-user benchmark against the final caipe-ui-prod image.
  • Add a cookie-auth load harness for /api/rbac/admin-tab-gates.
  • Measure authorized /api/admin/platform-config success path.
  • Confirm production TTL defaults are acceptable for admin freshness.

Docker Compose 1000-User S3 Run

This run validates the branch in docker-compose.dev.yaml with the UI sending audit events to audit-service, and audit-service writing Parquet to S3.

Runtime:

ItemValue
Compose filedocker-compose.dev.yaml
UI imageghcr.io/cnoe-io/caipe-ui:branch-s3-perf-prod
Audit imageghcr.io/cnoe-io/caipe-audit-service:branch-s3-perf
Audit modeAUDIT_LOG_BACKEND=service
Audit service backendAUDIT_SERVICE_BACKEND=s3
Users1,000 virtual HTTP users
Spawn rate100 users/sec
Duration60 seconds
Reportreports/1000u-branch-s3-compose-prod-20260620-001905.html

Build caveat:

  • The UI container was a branch runner image built from local next build standalone output because the standard Dockerfile npm ci layer stalled on this workstation.
  • Re-run with the official Dockerfile path before release sign-off.

Audit-service health after the paired 1000-user runs:

MetricValue
Backends3
Queue size0
Accepted events17,228
Flushed events17,228
Rejected events0
Failed flushes0

Container samples during the 60-second API run:

ContainerAvg CPUMax CPULast memory sample
caipe-ui-prod113.0%139.86%827.5 MiB
audit-service5.5%27.34%122.3 MiB

Interpretation:

  • S3 audit writes worked and drained fully.
  • audit-service was not the CPU bottleneck.
  • The full compose stack still shows high UI/API tail latency at 1,000 users.
  • Compare this only against other compose-container runs, not the faster local next start run on port 3005.

1000-user API result

The only recorded failures are the expected synthetic fake-conversation 400 responses from /api/chat/conversations [get].

MetricOld compose imageBranch compose + S3
Requests14,05416,662
Expected fake-get failures840947
Other failures00
RPS237.5276.6
Avg latency1909.7 ms2022.2 ms
p50660 ms530 ms
p957200 ms7700 ms
p999500 ms12000 ms
Max12622.8 ms24649.5 ms

Endpoint detail:

EndpointCountp50p95p99Failures
/api/auth/session3,836160 ms1000 ms11000 ms0
/api/rbac/admin-tab-gates2,845300 ms1700 ms10000 ms0
/api/admin/platform-config8731400 ms4800 ms11000 ms0
/api/chat/conversations [get]9471200 ms3400 ms12000 ms947 expected
/api/chat/conversations [list]4,5813100 ms6400 ms14000 ms0
/api/admin/stats1,795710 ms9800 ms15000 ms0
/api/dynamic-agents/available1,7852100 ms9700 ms11000 ms0

Browser route timing under 1000-user load

This route sweep ran while a separate 1,000-user Locust run was active.

RouteStatusElapsed
/20020516 ms
/chat20018187 ms
/skills2001895 ms
/task-builder2002766 ms
/workflows2001643 ms
/knowledge-bases2001110 ms

Summary:

MetricValue
Route failures0
Route p501895 ms
Route p9518187 ms
Route max20516 ms
Chat input fill17 ms
Reportreports/ui-elements-branch-s3-under-1000u-load-20260620-052158.json

Paired Locust run:

MetricValue
Reportreports/1000u-branch-s3-ui-under-load-20260620-002118.html
Requests18,125
Expected fake-get failures1,032
Other failures0
RPS241.0
Avg latency2473.4 ms
p50730 ms
p958500 ms
p9926000 ms

Follow-up: 1000-User Tail Latency

This branch improved the clean 300-user UI/API path and moved audit writes out of the UI request path. It did not prove a 1,000-user tail-latency win in the full docker-compose.dev.yaml stack.

Current conclusion

AreaStatus
Audit-service write pathHealthy at 1,000 users. S3 writes drained fully.
300-user UI/API pathStrong improvement. p95 dropped from 2405.6 ms to 354.7 ms.
1,000-user compose p50Slightly better. 660 ms to 530 ms.
1,000-user compose p95/p99Still poor. p95 7200 ms to 7700 ms, p99 9500 ms to 12000 ms.

Evidence

SignalFinding
audit-service CPUAverage 5.5%, max 27.34%. Not the bottleneck.
caipe-ui-prod CPUAverage 113.0%, max 139.86%. UI container was hot.
Audit queueQueue size 0 after run.
Audit flushes17,228 accepted, 17,228 flushed, 0 failed flushes.
Worst p95 endpoint/api/admin/stats at 9800 ms.
Next worst p95 endpoint/api/dynamic-agents/available at 9700 ms.
Chat list p95/api/chat/conversations [list] at 6400 ms.

Non-goals

  • Do not add more audit-service write-path changes unless new evidence shows audit-service is on the hot path.
  • Do not claim 1,000-user success based on the current compose run.
  • Do not tune blindly. Add measurements first, then optimize the endpoint or service that owns the tail.
  1. Make the benchmark production-like

    Run the 1,000-user workload against a deployment shape closer to production:

    • 2 or 3 caipe-ui replicas behind a load balancer.
    • Dedicated resource limits for UI, MongoDB, OpenFGA, PostgreSQL, and audit-service.
    • Same audit-service S3 backend used in the branch compose test.
    • Per-container CPU, memory, restart count, and network I/O captured for the whole run.
    • Per-endpoint Locust CSVs retained in reports/.
  2. Add request-path observability

    Capture enough timing data to explain the p95 and p99 instead of only seeing the final response time:

    • Node.js event-loop delay and heap usage for caipe-ui.
    • MongoDB query duration, collection name, and index usage for hot routes.
    • OpenFGA request count, batch size, latency, and error rate.
    • Cache hit, miss, and shared in-flight counts per cached route.
    • Route fanout per browser navigation.
  3. Reduce initial page-load fanout

    Home, Admin, and Chat currently create bursty API traffic during navigation. At 1,000 users that fanout becomes tail latency.

    Candidate changes:

    • Defer non-critical widgets until after first paint.
    • Lazy-load health, badges, and secondary admin panels.
    • Avoid polling during initial route hydration.
    • Coalesce duplicate same-user requests during one navigation.
    • Treat status panels as stale-while-revalidate where freshness allows it.
  4. Target the worst endpoints

    Start with the endpoints that dominate the 1,000-user p95:

    EndpointCurrent 1,000-user p95Next investigation
    /api/admin/stats9800 msPrecompute or cache aggregates. Avoid live multi-collection scans on page load.
    /api/dynamic-agents/available9700 msCache longer, refresh on config change, and avoid repeated discovery work.
    /api/chat/conversations [list]6400 msVerify indexes, trim projections, paginate sidebar data, and defer enrichment.
    /api/admin/platform-config4800 msCache config reads and separate authorized mutation checks from read hydration.
    /api/rbac/admin-tab-gates1700 msBatch secondary Slack/Webex checks and verify OpenFGA cache/replica settings.
  5. Scale the UI runtime

    One Next.js container is the likely choke point in the compose run. Validate:

    • Multiple caipe-ui replicas.
    • Horizontal scaling behavior under sticky and non-sticky sessions.
    • Whether per-process caches should remain local or move to Redis.
    • Node heap settings and event-loop delay under 1,000 users.
  6. Tune MongoDB and OpenFGA with measurements

    Only tune after query/RPC measurements are captured.

    • Confirm MongoDB indexes for chat list and admin stats queries.
    • Set MongoDB pool sizes from observed concurrency.
    • Enable or evaluate OpenFGA check/query cache.
    • Test OpenFGA with more replicas or larger connection pools.
    • Batch remaining secondary checks that still run as sequential loops.
  7. Improve broad audit reads separately

    The audit write path is healthy. Audit read path is acceptable for the new 5-minute default, but broad windows still scan and sort too much data.

    Follow-up options:

    • Cursor pagination for audit-service reads.
    • A compact audit index for broad historical queries.
    • Approximate counts for wide windows so the UI does not need full-window sorting before returning the first page.

Acceptance criteria for the follow-up

The next performance PR should be considered successful only when:

  • A 1,000-user run has zero unexpected failures.
  • The run uses a production-like multi-replica UI shape or clearly documents why a single UI container is still representative.
  • audit-service remains healthy with queue size 0 and failed flushes 0.
  • Per-endpoint p95 and p99 improve for the worst routes listed above.
  • Browser route timing under load improves for /, /chat, and Admin.
  • The benchmark report includes enough timing data to attribute the remaining tail to UI, MongoDB, OpenFGA, network, or browser route fanout.

Audit Read Window Benchmark

This run was added after the 1,000-user S3 benchmark made the RBAC Audit tab slow to open. The old default read path queried a 24-hour S3 window, scanned the day partition, loaded all matching Parquet objects, sorted all matches, and returned only the first page.

Changes:

  • RBAC Audit tab defaults to Last 5 min.
  • Time range options: 5m, 15m, 30m, 1h, 6h, 12h, 24h, 7d, and custom from/to.
  • /api/admin/audit-events forwards window and time_resolution.
  • audit-service accepts window and time_resolution.
  • S3 writes use minute-resolution object prefixes.
  • S3 reads use minute/hour/day-aware prefix listing and object timestamp pruning.
  • docker-compose.dev.yaml now passes AWS SDK env vars into audit-service for S3 mode.

Validation:

CheckResult
Python audit-service tests4 passed
UI RBAC audit tests15 passed
UI buildPassed
Audit-service readinessbackend=s3, failed_flushes=0

Read Timing

Before the read-window change:

PathWindowTime
Direct audit-service read24h default9.1 s
UI /api/admin/audit-events wrapper24h default17.5 s

After the read-window change:

PathWindowTimeRecordsTotal
Direct audit-service5m0.98 s3041
Direct audit-service15m2.98 s30221
Direct audit-service30m11.35 s3017,657
Direct audit-service1h11.00 s3017,657
UI API default5m0.98 s2323
UI API explicit15m2.66 s22

Browser smoke:

MetricValue
Page/admin?cat=security&tab=action-audit
Status200
Render time1.88 s
Default selected range5m
Spinner count after load0
Reportreports/audit-ui-window-smoke-final-20260620-054845.json

Interpretation:

  • The default RBAC Audit view no longer performs a full 24-hour S3 read.
  • Wider windows that include the 1,000-user benchmark burst still take longer because they include roughly 17k records.
  • The next readback improvement would be cursor pagination or a compact index so broad historical windows do not need exact full-window counts.