How to Benchmark AuthOS in a 1 vCPU, 1 GiB VM
Build a constrained KVM guest, run the AuthOS SQLite workload from the host, collect comparable results, and select a load for a longer test.

On this page
This tutorial reproduces the July 2026 AuthOS SQLite benchmark setup. AuthOS runs in an Ubuntu KVM guest with one vCPU, 1 GiB of assigned memory, and a 25 GiB virtual disk. k6 runs on the host so its CPU and memory use do not reduce the resources available to AuthOS.
The VM configuration represents the $10 monthly server budget used for this benchmark. It does not reproduce a particular provider’s processor, storage, network, or shared-CPU scheduling.
Requirements
Use a Linux x86-64 host with KVM access and enough free memory for the guest and load generator. The preserved run used:
- QEMU 10.2.1 with KVM;
- the Rust toolchain pinned by the AuthOS repository;
- k6 2.1.0;
cloud-localds, Python 3, OpenSSH,curl,openssl,sqlite3, and GNUbase64; and- permission to create a TAP network interface with
sudo.
Check the main tools before continuing:
test -r /dev/kvm && test -w /dev/kvm
qemu-system-x86_64 --version
k6 version
cloud-localds --help >/dev/null
The first command must exit successfully. If it fails, fix KVM access before creating the guest.
1. Build the measured AuthOS revision
Clone AuthOS and define the paths used by the remaining commands:
git clone https://github.com/drmhse/AuthOS.git
cd AuthOS
export AUTHOS_REPO="$PWD"
export AUTHOS_BENCH="$AUTHOS_REPO/api/benchmarks/sqlite-budget-vm"
export AUTHOS_BENCH_COMMIT=79e5a9440846f94dd31221c17b9b64930ec489bc
Build the revision used for the published measurements in a temporary worktree:
git worktree add /tmp/authos-benchmark-source "$AUTHOS_BENCH_COMMIT"
cargo build --release --locked \
--manifest-path /tmp/authos-benchmark-source/api/Cargo.toml \
--bin sso_sqlite
export AUTHOS_BINARY=/tmp/authos-benchmark-source/api/target/release/sso_sqlite
sha256sum "$AUTHOS_BINARY"
The preserved binary had this SHA-256:
e3386a969271e4060f2ffdb2199e27b3276021b1cfa9be87f0787355c9ef884f
Record your hash even if it differs. A different compiler or linker can produce a different binary from the same source revision.
2. Create the benchmark database and keys
Run the fixture preparation script on the host:
AUTHOS_BINARY="$AUTHOS_BINARY" "$AUTHOS_BENCH/prepare-seed.sh"
The script performs the following operations:
- creates a fresh SQLite database;
- applies AuthOS migrations;
- inserts the synthetic tenant, service, user, session, and device-client records;
- generates an RSA key pair;
- verifies the benchmark login; and
- prints the seed database SHA-256.
The generated files are under:
api/benchmarks/sqlite-budget-vm/.work/
The fixture credentials, encryption value, and keys are for this test only. Do not use them in an AuthOS deployment.
3. Download and verify the Ubuntu image
Download the Ubuntu 26.04 cloud image used by the VM launcher:
curl --fail --location \
--output /tmp/ubuntu-26.04-server-cloudimg-amd64.img \
https://cloud-images.ubuntu.com/releases/26.04/release/ubuntu-26.04-server-cloudimg-amd64.img
Verify its SHA-256:
printf '%s %s\n' \
0826c5005ebc70edcfc4519e5d65eca766782f16426231c4c3e92b811ba8df0b \
/tmp/ubuntu-26.04-server-cloudimg-amd64.img \
| sha256sum --check
Do not continue if verification fails. The VM launcher performs the same check and will reject a different image.
4. Create the host network and start the VM
Create a temporary SSH key:
ssh-keygen -t ed25519 -N '' -f /tmp/authos-benchmark-key
Create the TAP interface used by the host and guest:
sudo ip tuntap add dev tap-authos mode tap user "$USER"
sudo ip addr add 192.168.76.1/24 dev tap-authos
sudo ip link set tap-authos up
Start the VM:
AUTHOS_VM_IMAGE=/tmp/ubuntu-26.04-server-cloudimg-amd64.img \
AUTHOS_VM_PUBLIC_KEY=/tmp/authos-benchmark-key.pub \
"$AUTHOS_BENCH/vm/launch.sh"
The launcher creates a 25 GiB copy-on-write disk and starts QEMU with one vCPU and 1,024 MiB of assigned memory. The host uses 192.168.76.1; cloud-init assigns 192.168.76.2 to the guest.
Wait until SSH succeeds:
until ssh -o ConnectTimeout=2 \
-o StrictHostKeyChecking=accept-new \
-i /tmp/authos-benchmark-key \
benchmark@192.168.76.2 true
do
sleep 2
done
5. Copy AuthOS and the fixture into the guest
Create the guest directories:
ssh -i /tmp/authos-benchmark-key benchmark@192.168.76.2 \
'install -d -m 700 /home/benchmark/authos/work'
Copy the binary, runner, database, and keys:
scp -i /tmp/authos-benchmark-key \
"$AUTHOS_BINARY" \
"$AUTHOS_BENCH/run-server.sh" \
"$AUTHOS_BENCH/.work/seed.db" \
"$AUTHOS_BENCH/.work/private.pem" \
"$AUTHOS_BENCH/.work/public.pem" \
benchmark@192.168.76.2:/home/benchmark/authos/
ssh -i /tmp/authos-benchmark-key benchmark@192.168.76.2 \
'mv /home/benchmark/authos/{seed.db,private.pem,public.pem} /home/benchmark/authos/work/'
6. Start AuthOS in the guest
Open a terminal for the server and run:
ssh -t -i /tmp/authos-benchmark-key benchmark@192.168.76.2 \
'chmod 700 /home/benchmark/authos/sso_sqlite \
/home/benchmark/authos/run-server.sh && \
AUTHOS_BINARY=/home/benchmark/authos/sso_sqlite \
AUTHOS_BENCH_WORK_DIR=/home/benchmark/authos/work \
AUTHOS_BENCH_HOST=0.0.0.0 \
AUTHOS_BENCH_PUBLIC_URL=http://192.168.76.2:3301 \
/home/benchmark/authos/run-server.sh'
Keep this terminal open. The runner sets the file-descriptor limit to 65,535, restores seed.db to a new working database, and starts AuthOS on port 3301.
From another host terminal, verify readiness:
curl --fail http://192.168.76.2:3301/health/ready
Do not start the load test until this request succeeds.
7. Run a smoke test
Return to the AuthOS repository in a host terminal and restore the path variables if this is a new shell:
cd /path/to/AuthOS
export AUTHOS_BENCH="$PWD/api/benchmarks/sqlite-budget-vm"
export AUTHOS_BENCH_BASE_URL=http://192.168.76.2:3301
Run one virtual user for 15 seconds:
"$AUTHOS_BENCH/run-case.sh" 1 15s smoke-local
The command must exit with status 0. Check these files if it fails:
api/benchmarks/sqlite-budget-vm/benchmark-results/smoke-local/k6-console.txt
api/benchmarks/sqlite-budget-vm/benchmark-results/smoke-local/summary.json
/home/benchmark/authos/work/server.log # inside the guest
run-case.sh requires a new run ID each time. Remove an unwanted result directory or choose a different ID before repeating a case.
8. Configure source addresses for high-load cases
The measured AuthOS revision applies a per-IP governor to the device routes. If every k6 virtual user shares 192.168.76.1, a high-load run measures that rate limit instead of server throughput.
To model clients with separate source addresses, add a pool to the TAP interface:
for octet in $(seq 10 100); do
sudo ip addr add "192.168.76.${octet}/32" dev tap-authos
done
export K6_LOCAL_IPS=192.168.76.10-192.168.76.100
Use this pool when the test is intended to measure AuthOS throughput across many client identities. Do not use it when the test is intended to measure behaviour behind a shared NAT address; in that case, HTTP 429 responses are part of the result.
9. Run the load matrix
Restart run-server.sh before every case. In the server terminal, press Ctrl-C, repeat the command in step 6, and wait for the readiness request to succeed. Each restart copies the original seed database, so database state from one case does not affect the next.
Run the short cases from the host:
"$AUTHOS_BENCH/run-case.sh" 100 30s ramp-0100-local
"$AUTHOS_BENCH/run-case.sh" 500 45s ramp-0500-local
"$AUTHOS_BENCH/run-case.sh" 1000 60s ramp-1000-local
"$AUTHOS_BENCH/run-case.sh" 2000 60s ramp-2000-local
"$AUTHOS_BENCH/run-case.sh" 3000 60s ramp-3000-local-1
"$AUTHOS_BENCH/run-case.sh" 3000 60s ramp-3000-local-2
"$AUTHOS_BENCH/run-case.sh" 3000 60s ramp-3000-local-3
"$AUTHOS_BENCH/run-case.sh" 4000 60s ramp-4000-local-1
"$AUTHOS_BENCH/run-case.sh" 4000 60s ramp-4000-local-2
"$AUTHOS_BENCH/run-case.sh" 4000 60s ramp-4000-local-3
"$AUTHOS_BENCH/run-case.sh" 5000 60s ramp-5000-local
Run longer cases only after the short matrix identifies a load range with no failures:
"$AUTHOS_BENCH/run-case.sh" 2000 300s soak-2000-local-5m
"$AUTHOS_BENCH/run-case.sh" 3000 300s soak-3000-local-5m
The workload uses k6’s constant-vus executor. Each virtual user completes an operation, waits one second, and repeats. This is a closed workload: when AuthOS slows down, the next iteration also starts later. Use an arrival-rate executor for a separate test when requests must continue to arrive at a fixed rate. The k6 open and closed model documentation
explains the difference.
10. Monitor AuthOS during each run
Open another terminal and watch guest CPU, runnable processes, memory, and I/O:
ssh -i /tmp/authos-benchmark-key benchmark@192.168.76.2 \
'vmstat -w 1'
Check AuthOS memory and its active file-descriptor limit:
ssh -i /tmp/authos-benchmark-key benchmark@192.168.76.2 \
'pid=$(pgrep -n sso_sqlite); \
grep -E "VmRSS|VmHWM|Threads" "/proc/$pid/status"; \
grep "Max open files" "/proc/$pid/limits"'
Check the server log after a failed case:
ssh -i /tmp/authos-benchmark-key benchmark@192.168.76.2 \
'tail -n 100 /home/benchmark/authos/work/server.log'
Also confirm that k6 is not using all host CPU or memory. If the host load generator saturates, the run does not establish the guest’s limit.
11. Choose the operating load from the results
For each case, record:
operation_failures;- completed AuthOS operations per second;
- HTTP p99;
- guest CPU use;
- AuthOS peak RSS; and
- unexpected HTTP 429, 500, or 503 responses.
Use the following checks:
| Observation | Action |
|---|---|
| Failures occur while guest CPU is low | Inspect server.log and /proc/<pid>/limits before changing SQLite settings. |
| Device routes return 429 from one source address | Decide whether the test should represent one shared address or many client addresses. Configure K6_LOCAL_IPS only for the latter. |
| Operations per second stop increasing while p99 and CPU rise | Use a lower load for the longer soak. |
| k6 saturates the host | Move k6 to another machine or reduce the generated load. Do not report the run as the guest limit. |
| A five-minute run passes | Run a longer case for maintenance work that occurs after five minutes. |
The preserved short runs produced:
| VUs | Operations/s | HTTP p99 | Guest CPU | Failures |
|---|---|---|---|---|
| 2,000 | 1,917 | 350 ms | 60% | 0% |
| 3,000 | 2,588 | 603 ms | 86% | 0% |
| 4,000 | 2,737 | 1.09 s | 94% | 0% |
| 5,000 | 2,709 | 1.99 s | 94% | 0% |
The 5,000-VU run completed fewer operations than the 4,000-VU median and had higher p99. The longer comparison was therefore run at 2,000 and 3,000 VUs:
| VUs | Duration | Operations/s | HTTP p99 | Guest CPU | Peak RSS | Failures |
|---|---|---|---|---|---|---|
| 2,000 | 5 min | 1,968 | 154 ms | 68% | 196 MiB | 0% |
| 3,000 | 5 min | 2,506 | 1.06 s | 95% | 277 MiB | 0% |
For this host, AuthOS revision, database, and workload, 2,000 VUs was selected because it completed the run without failures and retained more CPU and latency headroom than 3,000 VUs.
These figures are not an MAU estimate. They also exclude a reverse proxy, TLS, internet latency, provider storage, backups, key rotation, and upgrades. Repeat the same workload on the intended provider before using the result for deployment sizing.
AuthOS device codes expire after 15 minutes. Use the 30-minute case when measuring expiry cleanup under load:
"$AUTHOS_BENCH/run-case.sh" 2000 30m cleanup-2000-local-30m
After the 30-minute case, stop AuthOS so SQLite checkpoints the WAL, then copy the database to the host:
scp -i /tmp/authos-benchmark-key \
benchmark@192.168.76.2:/home/benchmark/authos/work/authos.db \
/tmp/authos-cleanup.db
Count all device codes and the subset that have expired:
sqlite3 -header -column /tmp/authos-cleanup.db \
'SELECT COUNT(*) AS total,
COALESCE(SUM(CASE WHEN expires_at < CURRENT_TIMESTAMP THEN 1 ELSE 0 END), 0) AS expired
FROM device_codes;'
The expired value should be zero. A non-zero value means the cleanup job did not remove all codes that had passed their expiry time.
12. Stop the VM and remove temporary files
Stop QEMU and remove the TAP interface:
kill "$(cat "$AUTHOS_BENCH/.work/vm/qemu.pid")"
sudo ip link del tap-authos
Remove the temporary key and worktree:
rm -f /tmp/authos-benchmark-key /tmp/authos-benchmark-key.pub
git worktree remove /tmp/authos-benchmark-source
The result directories remain under api/benchmarks/sqlite-budget-vm/benchmark-results/. Keep the environment file, k6 console output, summary JSON, server log, and resource samples for every result you publish.



