Sizing and measured performance
The first question after "is it secure" is "how many developers will it hold". This document answers it with numbers from a run anyone can repeat together with the caveats that make those numbers honest. Where the measurement does not support a claim the claim is not made.
Everything below was measured rather than modelled.
The measurement
Thirty environments on a kind cluster installed through the Helm chart exactly as a
customer installs it and held under load for five minutes. The script is hack/load.sh.
It creates N environments through the API and waits for each to report Ready. It then drives
the read path with concurrent clients and soaks with several writers appending to the audit
chain. Finally it verifies the chain. Results land in .load-results/.
Results
Measured 2026-09-12 on an Apple M4 Pro with 14 cores and 24 GiB RAM on macOS 26.5.2.
Docker Desktop had 14 CPUs. The cluster was single-node kind with one API replica and one
operator replica.
Environment creation
| Environments requested | 30 |
| Reached Ready | 30 of 30 |
| Time to Ready | p50 6.3 s rising to p95 7.3 s with a max of 7.3 s |
| Wall clock for all 30 | 34 s with six creates in flight |
Measured from the API accepting the create to the Environment reporting Ready — the reconcile and the pod scheduling and the routing update. That is what a developer actually waits for.
These were shared-mode environments that deploy a prebuilt image. Building from source adds the build. That depends on your repository and registry and is not included here.
API read load
| Load | 20 concurrent clients for 60 s on GET /environments |
| Throughput | 290 requests/second |
| Errors | 0 non-2xx out of 17411 |
| Latency | p50 13 ms rising to p95 24 ms and p99 78 ms with a max of 128 ms |
This is the endpoint a dashboard polls with 30 environments in the workspace.
Soak with sustained audit appends
| Duration | 5 minutes with 4 concurrent writers |
| Writes | 12640 with 0 errors |
| Latency | p50 8 ms rising to p95 15 ms and p99 17 ms with a max of 28 ms |
| Audit chain after | verifies intact |
Every write appends to the hash chain and that cannot be parallelised inside the database — each entry hashes onto the previous one. Four concurrent writers is the shape that would expose that as a bottleneck and at this rate it is not one: write latency is lower than read latency because a read serialises 30 environments and a write appends one row.
What this supports
Tested to 30 concurrent environments on a 14-core laptop with the API answering 290 reads/second at a p99 of 78 ms and sustaining 42 audited writes/second with the chain intact.
For a rough sense of scale: a team of 200 developers where each creates a couple of environments a day and has a browser polling the dashboard is nowhere near any of these figures. The single-replica decision assumed "a few hundred developers doing occasional writes". Nothing measured here contradicts that.
What this does not support
Read this part before quoting the numbers above.
- kind is not your cluster. Every node is a container on one machine with local storage and no real network. A real cluster has slower disks and slower networking and other tenants. Treat these as a floor for the software and not a capacity plan for your hardware.
- 30 environments is what this machine held and not a product limit. It is the largest run that fit comfortably. The numbers were flat across it with no sign of a knee. What happens at 300 is unmeasured and we will not guess.
- No build was involved. Every environment ran a prebuilt image.
- Memory over the soak is unmeasured. kind runs no metrics-server so the before/after
figures came back
n/a. A leak over five minutes would not necessarily show anyway. A longer soak on a cluster with metrics is the honest way to answer this and it has not been done. - One workspace with one application and two services per environment. A wider application changes the reconcile cost.
- Shared mode only. A full-mode environment runs the whole application and costs what one full stack costs. Full mode was not part of this run.
- Neither terminal sessions nor SIEM forwarding nor evidence upload were running concurrently.
Rules of thumb
Until you have measured your own cluster:
- API: 1 replica at 500m CPU / 512 MiB is comfortable at this load. It is single-replica by design. Do not scale it horizontally. You do not need to.
- Operator: 1 replica. Reconciles are short and the work is per-environment.
- Data volume: the audit log only grows. At roughly 42 writes/second sustained — far above
any real installation — the soak wrote 12640 entries in five minutes. A real install of 200
developers writes a few thousand entries a day. Size the volume for years and watch
nezal_data_volume_free_bytes. Archive old ranges to object storage. - Per environment in shared mode: whatever the services you override request. The shared baseline is paid once per application and not once per developer.
- Per environment in full mode: one whole stack. Size the cluster for one stack per developer who has an environment and use the Policy's environment limits and TTL to cap it.
Repeating this
Run hack/load.sh to repeat it on kind. ENVS=60 SOAK=30m hack/load.sh changes the size
and the length. On your own cluster any load tool works: create environments through the API
or the CLI in parallel and hold them for the soak period. Read the API latency from
nezal_api_request_duration_seconds at /metrics and the time to Ready from each
Environment's conditions.
The script fails if the audit chain does not verify at the end. That is not decoration: this measurement is how a canonicalisation bug in the audit chain was found. The chain reported tampering on a log nobody had touched.