CAIN-42 CAIN Studio

Developer documentation

Benchmarks

Last reviewed 31 August 2026

All docs

Every figure on this page was measured on the live hosted deployment with bench/benchmark.py. Nothing is modelled, extrapolated, or rounded to something friendlier. Where a mode could not be measured, it says so and reports nothing.

Reproduce it yourself:

CAIN_API_KEY=<your key> python3 bench/benchmark.py --samples 300 --concurrency 16

Current figures (2026-10-01)#

Since 2026-09-26 every hosted decision waits for one round of the PBFT cluster (4 nodes in 4 regions) before it returns. That round dominates latency, so the August figures further down describe a path that no longer exists.

Measured with curl against the public demo, POST /fabric/try?scenario=safe-read, sequential requests from the gateway host, n = 8 per run. The consensus figure is the consensus.latency_ms field each response carries.

runtotal medianminmaxconsensus stage median
2026-10-01 05:30Z0.73 s0.61 s1.30 s654 ms
2026-10-01 06:40Z (just after a restart)1.02 s0.80 s2.02 s655 ms
  • What is slow: the consensus round (about 650 ms of the total). The cluster's

nearest peers are 16 ms and 49 ms away, so most of it is not network: one replica shares a 2-vCPU host with the gateway.

  • The fast path: with CAIN_AUTH_SNAPSHOT=1 (set in production) an agent its

owner has granted the fast path (POST /fabric/agent-keys/{id}/fast-path) is checked against a quorum-committed authorization snapshot instead of waiting for a round. Peer measurements put that stage at about 0.8 ms; it has not yet been measured end to end for a customer agent, so no total is given here.

  • Throughput: about 2.6 decisions per second at saturation on the current

host (forensic audit, 2026-10-01). The demo is limited to 20 requests per hour per address.

Historical latency (2026-08-17, before consensus was on the path)#

Measured 2026-08-17 against https://cainstudio.online, deployment in enforce mode. 1200 samples per mode after 5 discarded warm-up requests.

modemedianp95p99nok
OFF (no decision path)10.51 ms15.58 ms18.78 ms1200100%
ENFORCING (full path)20.79 ms32.00 ms44.40 ms1200100%
SHADOW*not measured*
ENFORCING + ActionProof91.27 ms130.06 ms181.92 ms1200100%

Fabric overhead: 10.28 ms median, 16.42 ms p95. ActionProof adds 70.48 ms median on top of that, when a plan is submitted.

p99 is reported here because 1200 samples is enough to place it. At 300 samples it was withheld as n/a rather than printed — see below.

Throughput#

modeconcurrencyrpssuccessful rpsnon-200
ENFORCING2499.599.5none

1007 requests over 10 seconds, every one a 200. Note that raising concurrency from 16 to 24 did not raise throughput (108 rps → 99 rps): the decision path is not concurrency-limited at this level, so adding load adds queueing rather than work. That is worth knowing before sizing a deployment, and it is why the figure is published with its concurrency attached rather than as a bare "rps". The successful-rps column is reported separately from total rps on purpose: a throughput number with the errors hidden is the most common way a benchmark lies, and 400 rps where a third of responses are 429 is not 400 rps of useful work.

How to read these numbers honestly#

OFF is not "CAIN uninstalled". It is a request to the same host on a route that does not enter the decision path. That isolates fabric cost from network latency to this box, which is the number you actually want when deciding whether to adopt. It is *not* a measurement of your agent calling its own tools locally with no CAIN at all — that number depends entirely on your network, not ours.

p99 is withheld when there are too few samples. The benchmark refuses to print a percentile unless it has at least 5× the samples needed to place it, because a "p99" from 300 observations is roughly the third-worst one wearing a formal name. The figures above come from 1200 samples, which is why p99 appears. Run with --samples 300 and it correctly reports n/a instead.

SHADOW is not measured, and not estimated. Shadow versus enforcing is a live property of the deployment, set by a global switch. Measuring shadow would mean flipping that switch on production traffic, so the benchmark reports not_measured with the reason rather than printing a plausible number.

ActionProof is the expensive stage, and that is expected. It discharges real SMT queries about a submitted plan. 68 ms to prove a two-step plan cannot violate your constraints is a different kind of work from a policy lookup, and it only runs when you submit a plan. If you never send one, you never pay for it — the stage reports skipped.

Failure behaviour#

The benchmark also measures what happens when a stage has nothing configured. A verification stage with no profile returns not_configured promptly; it does not hang waiting for a verifier that has no constraints to check. That matters because a control which stalls under a missing configuration becomes an outage rather than a refusal.

What is not benchmarked yet#

  • Sustained load over hours. These are seconds-long windows. Nothing here

says anything about behaviour under sustained pressure, memory growth, or connection-pool exhaustion over a day.

  • Concurrency beyond 24. Two levels measured (16 and 24); throughput did not

improve, so the useful ceiling is somewhere at or below 16 concurrent callers on this hardware. Nothing here characterises 100+.

  • Self-hosted MCPGate. Its performance depends on your hardware, so a number

measured on ours would be misleading rather than useful.

  • Cold start. Warm-up requests are discarded, so these are steady-state

figures. The first request after a deploy pays TLS setup and is slower.

These gaps are listed rather than quietly omitted, and they are on what CAIN does not do yet.