Skip to main content

Measured performance, quotas, RTO and RPO

Requirements: OBSE-013, OBSE-014, LLM_-009, RELE-014, OBSE-010. Version française : ../performance-results.md.

Every figure on this page comes from a run of bash tools/perf/run.sh on the host described below, on 2026-10-30 (results folder /tmp/eai-perf-final). Nothing is estimated. The figures depend on the host (WSL2 shared with other processes): they are a local capacity reference, not a service commitment.

Host​

nproc: 6
uname -sr: Linux 6.6.87.2-microsoft-standard-WSL2
free -g:
total used free shared buff/cache available
Mem: 47 20 3 0 23 26
Swap: 96 1 94

Commands​

bash tools/perf/run.sh /tmp/eai-perf-final
cd services/control-plane && .venv/bin/python -m pytest -q tests/test_perf_measurements.py tests/test_finops_attribution.py tests/test_lineage_exploration.py

tools/perf/loadgen.py (Python asyncio and httpx, no other tool) starts on free ports a throwaway PostgreSQL (pgserver of services/control-plane/.venv, every migration applied), the fake OpenAI compatible provider tools/perf/fake_upstream.py and the Control Plane under uvicorn (one process, EAI_AUTH_DEV=1, EAI_EGRESS_PRIVATE_ALLOWLIST=127.0.0.1/32, global rate limiting off so that the platform is measured and not the limiter). CPU time (utime + stime) and RSS of the API process are read from /proc/<pid>/stat and /proc/<pid>/status. Results go to load.json and load.md. tools/perf/resilience.py writes resilience.json.

The k6 scripts of deploy/loadtest were not run: the k6 binary is AGPL-3.0 and may only be used as a local test tool, never distributed; the in-house generator is used instead.

Throughput and latency (OBSE-014)​

400 requests per concurrency level after 4 warm-up requests. Scenarios: health (GET /healthz), model_gateway_chat (POST /v1/gateway/chat/completions to the fake provider: quota reservation, PEP, DLP, call, Usage Event, commit), tool_gateway_invoke (POST /v1/tools/{name}/invoke on the builtin echo handler: the egress guard refuses private destinations for tenant tools by design, so the measured path is the full tool gateway without a network hop). The CPU column is the CPU time used by the API during the level; RSS is read at the end of the level.

ScenarioConcurrencyRequestsErrorsError rateThroughput (req/s)p50 (ms)p95 (ms)p99 (ms)API CPU (s)API RSS (MiB)
health140000.000368.132.63.374.470.77127.7
health840000.000502.5114.5719.9382.080.74128.3
health3240000.000421.4754.79213.32299.120.73130.2
model_gateway_chat140000.00013.0973.9291.92110.6822.43209.7
model_gateway_chat840000.00019.9397.87523.22565.4234.52222.3
model_gateway_chat3240000.00020.41548.021960.262147.0650.18261.3
tool_gateway_invoke140000.00034.5625.6546.7166.936.16261.2
tool_gateway_invoke840000.00046.96163.84244.77302.097.61261.2
tool_gateway_invoke3240000.00042.2727.72938.551039.719.25261.5

Reading: one uvicorn process tops out around 20 model gateway calls per second on this host; beyond that, concurrency only adds waiting (latency grows linearly). Scaling goes through several replicas, not measured here (see blocked items).

Quotas (LLM_-009)​

LimitConfigurationObserved answersError codeRecovery
Requests per minutetool manifest rate_limit.requests_per_minute: 55 x 200 then 3 x 429, Retry-After: 60rate_limited200 again 60.6 s after the first refusal (60 s sliding window)
TokensPOST /v1/quotas tenant, period: hour, limit_tokens: 1004 x 200 (96 tokens used) then 429 before any provider callquota_exceeded200 as soon as the limit is raised with PUT /v1/quotas/{id}
Concurrent reservationsPOST /v1/quotas tenant, period: day, limit_cost: 0.30, reservations of 0.10 through POST /v1/quotas/reserve3 x 201 then 2 x 429quota_exceeded201 as soon as they are released (POST /v1/quotas/{id}/release)

The quota service has no period shorter than one hour (request, run, hour, day, month): a tokens per minute limit does not exist as such, the finest measured token limit is the hourly one. Recovery of the hourly limit after its window (one hour) was not waited for; recovery was measured by raising the limit.

Cost attribution (OBSE-013) and lineage (OBSE-010)​

Tenant ids are deliberately not Prometheus labels (cardinality and isolation). Attribution per tenant, model and agent is proven by services/control-plane/tests/test_finops_attribution.py through GET /v1/finops/usage, GET /v1/finops/summary?group_by=model|agent|tenant and the analytics export rows sent to ClickHouse, with no cross-tenant leakage. The lineage walk from answer to source and back is proven by services/control-plane/tests/test_lineage_exploration.py.

RTO and RPO (RELE-014)​

RTO and RPO targets: to be set by the client. Only measured values are shown.

ComponentMeasureMeasured value
PostgreSQLdatabase size (migrations + 50,090 probe rows)18,706,915 bytes
PostgreSQLbackup scripts/pg_backup.sh (custom format, checked with pg_restore --list)0.16 s, 816,324 bytes
PostgreSQLrestore scripts/pg_restore.sh into an empty database, with the migration check9.059 s
PostgreSQLdata loss of a logical backup (writes every 10 ms)50 rows, 0.636 s window written after the snapshot
Redisreplica promotion (REPLICAOF NO ONE) after SIGKILL of the primary, up to the first accepted write4.071 s
Rediskeys acknowledged by the primary and missing on the promoted replica0 of 350

The loss of a logical backup is everything written after the snapshot: the real RPO of a restore is the interval between two backups plus this window. The Redis promotion time runs from the SIGKILL to the first write accepted by the promoted replica; failure detection by Sentinel or the operator is not included. A first trial the same day gave 2.907 s of restore and 0.0021 s of promotion: the variance of the shared host is high, these values are not bounds.

Blocked items​

  • GPU measurements: BLOCKED, no GPU on this host.
  • High availability and scale in a cluster (several replicas, CloudNativePG PostgreSQL, Redis Sentinel): BLOCKED, no cluster available; nothing was applied to a cluster or a cloud account.