Method
scripts/bench-idle.sh builds the control plane, boots it in dev mode in a
throwaway data directory, and for each app count N:
- Creates apps up to N through the HTTP API and stops each one, so every app is suspended. No containers run and no images are pulled.
- Waits 5 seconds, then samples the process’s cumulative CPU time across a 60 second idle window and reads RSS at the end.
- Sends 200 sequential GET requests per endpoint over one connection and reports p50 and p95 latency in milliseconds.
Results
Apple M4 Max (14 cores), macOS, dev build, same-machine loopback client.
Latency in ms, p50 / p95:
GET /system/status caches Docker disk usage for 30 seconds (errors are never cached), so it does not pay the roughly 370 ms docker system df cost on every request. GET /system/doctor takes about 220 ms because it runs live checks (Docker, database, and others) by design, and it is not on any polling path.
Caveats
- One machine, one run per configuration. Expect run-to-run variance of roughly 10 to 15% on RSS and CPU.
- No containers are running. Real apps add Docker event, health probe, and metrics collection load that this benchmark does not measure.
- Dev build in dev mode, not a release binary. Loopback client, so no network latency is included.
- Apps are suspended, which exercises the reconcile resync path but not running-app reconciliation.
Benchmark script notes
- The default API rate limits throttle a 200-request burst, so the script raises
APP_API_RATE_LIMIT_READ_RPMandAPP_API_RATE_LIMIT_WRITE_RPM. - The brand file is resolved relative to the working directory. Run the script from the repo root (it does), or set
APP_BRAND_FILEwhen running the binary from elsewhere.
Re-running
scripts/bench-idle.sh
The defaults are N = 0, 100, 500 and a 60 second window. For a quicker run:
BENCH_REQUESTS (requests per endpoint, default 200) and
BENCH_PORT. It needs bash, curl, ps, awk, and the Go toolchain, and cleans up
its data directory on exit.