QuantyKube

Benchmarks against etcd

All pages

From docs/BENCHMARKS.md in the QuantyKube repository.

On this page

Measured with scripts/bench.sh, which builds etcd's own benchmark tool from the etcd release it compares against and runs both stores on the same machine, the same disk and the same kind of socket. Every number here can be reproduced with it; numbers that cannot are not published (ADR-002).

Run on 20 October 2026, QuantyKube at the group commit change.

Machine

CPU2 vCPU, Intel Xeon @ 2.10GHz, virtual machine
Memory8 GB
Diskvirtual block device, ext4, fdatasync 56 to 130 microseconds
etcdv3.6.15, one member, default settings
TransportUnix socket for both
Tooletcd tools/benchmark built from v3.6.15

The benchmark client runs on the same two cores as the store, which favors neither side but makes everything CPU bound sooner than on real hardware. A slower disk would hurt whichever store syncs more often. The real numbers come from the rented servers before 17 November.

Results

Median of three runs, fresh store each run, 256 byte values.

Workloadetcd req/sQuantyKube req/setcd p99 msQuantyKube p99 ms
put, 1 client1,3929822.092.54
put, 16 clients7,8584,4216.617.11
txn put, 16 clients7,4634,5286.647.56
range one key, linearizable, 16 clients19,28735,3664.062.47
range one key, serializable, 16 clients20,49734,5673.672.27
watch event delivery, 100 watchers2.021.82

After the 16 client put run:

etcdQuantyKube
peak memory47.4 MiB20.8 MiB
data on disk130 MB169 MB

With a k3s control plane on top, creating, listing and deleting 500 ConfigMaps, two runs each, measured with the build before group commit:

etcdQuantyKube
API server ready3.4 s3.3 to 3.4 s
store memory, idle44 MiB15 MiB
apply 500 ConfigMaps2.5 to 2.7 s2.6 to 3.0 s
list them0.14 to 0.19 s0.17 to 0.18 s
data on disk after124 MB45 MB

What this says

Writes are slower. A single client gets about 70% of etcd's rate: QuantyDB commits with two fsyncs where etcd needs about one, and a lone writer has nobody to share them with. Sixteen clients get about 56% of etcd's rate. Before group commit (ADR-012) that was 20%; the store is now bound by CPU per write, mostly QuantyDB's commit work, and that is the next thing to profile.

Reads are faster, by about 1.8 times for a single key. A read here is a lookup in a copy on write snapshot with no lock held against writers. This workload reads one tiny key, so it measures the request path, not storage at scale.

Memory is a third to a half of etcd's, in every run.

Disk is a trade. Each commit writes new pages for everything it touched and keeps the old ones for the history window, an hour by default. Under the put benchmark, which writes 20,000 new keys as fast as it can, that is 169 MB against etcd's 130. Under the Kubernetes workload it is 45 MB against 124. Space is reused once history ages out of the window; the file does not shrink, and etcdctl defrag is not supported.

Retention changed after these runs (ADR-014): snapshots now live a minute and the history lives in the change log. A 15 minute soak with a k3s control plane under steady writes, 12,368 revisions with --retain 2m --snapshots 30s, settled at 51 MB on disk and 31 MB of memory, where the earlier design grew without bound until the API server compacted.

In a Kubernetes control plane the stores are even. The API server takes the same time to become ready and to apply and list objects on both. Deleting 500 objects took 100 seconds on both, which is a limit of kubectl delete, not of either store.

Earlier anomalies, explained

The first run of these benchmarks once saw k3s take 128 seconds to become ready on QuantyKube, with k3s logging "certificate signed by unknown authority" for its own port 6443. The same message appeared later when two k3s instances ran on one machine at once: the new one was talking to the old one, which still held the port. It is a property of the test setup, not of the store, and it did not recur once runs were kept apart.

Before group commit, the 16 client put workload ran at 564 to 1,494 requests a second and the data file grew by up to 25 KB per write. Both improved with ADR-012: 4,399 to 4,515 requests a second, and about 8.5 KB per write.

Pod start: the runtime against containerd

scripts/podstart.sh, on the same 2 vCPU machine, with only one runtime up at a time. Both got the same crictl v1.34.0, the same busybox image (manifest sha256:b7f3d86d...), the same pod and container definitions and the same CNI bridge network (plugins v1.9.1). containerd v2.4.2 ran runc v1.3.2 with the cgroupfs driver and registry.k8s.io/pause:3.10.1 as its sandbox image. 30 pods each, after one untimed warm-up, two runs:

RunPodSandboxto container runningmemory, 10 pods
QuantyKube37 to 39 ms (p90 44 to 45)95 to 98 ms (p90 105 to 108)7 MiB, 1 process
containerd107 to 109 ms (p90 118 to 126)223 to 236 ms (p90 245 to 268)179 MiB, 11 processes

The memory is the resident size of the runtime's processes: containerd and its ten shims, without the ten pause processes, against the one QuantyKube process that holds no process per pod (ADR-017).

What makes this less than a final claim:

  • The machine has cgroup v1. containerd put each pod into a cgroup; QuantyKube, which supports only v2 (ADR-020), created none. This is the largest known difference in work done, and the rented nodes, on cgroup v2, will measure both doing it.
  • Each step is a separate crictl process, 12 to 17 ms to start, in both columns alike; a kubelet calls over a held connection, so the real gap in absolute time is the same and the ratio larger.
  • containerd's OOM score adjustment had to be restricted, because the test machine lacks CAP_SYS_RESOURCE.