Skip to content

Performance

Measured on 2026-10-19. The Go benchmarks are on the development machine (14 threads); the box's numbers on the lab VMs (lab.md), so the routed numbers compare choices rather than promise a speed.

strongholdd's work per change

go test -bench . ./internal/... (one run of each):

What Time Allocated
Load and validate the example config (config.Load) 0.83 ms 0.26 MB
Validate a big config (1000 more rules, 60 reservations) 0.46 ms 0.13 MB
Marshal that big config to YAML (each change does this twice) 13.6 ms 15.9 MB
Render firewalld's zones and policies (example) 0.28 ms 0.22 MB
... the big config (1000 more rules) 2.6 ms 1.3 MB
Render the NAT table 0.005 ms 4 KB
Render Kea, Knot and Unbound (example) 0.13 ms 60 KB
Render NetworkManager's profiles (example) 0.09 ms 48 KB

Marshalling is the one heavy part, all of it in the YAML library's emitter; 14 ms for a thousand rules is nothing next to a person making the change, so it stays.

The API

On stronghold-fedora (2 vCPUs), over the Unix socket, 50 requests each; the floor is curl's own start (~4.5 ms):

Route Per request
GET /v1/version 4.5 ms
GET /v1/status 5.0 ms
GET /v1/config 6.8 ms
GET /v1/system/metrics 6.9 ms
GET /v1/firewall/counters 18 ms (runs nft -j)
GET /v1/functions 29 ms (was 82: one systemctl is-active for all the programs on the host, not one each)

strongholdd itself: 20 MB resident, 9 MB in its cgroup, idle at 0.1% CPU.

Routed traffic

Through the package-mode box (pkg, 2 vCPUs): pkgclient on its LAN, iperf3 server on dhcpsrv beyond its WAN, NATed; each figure the mean of two 10 s runs.

TCP, 1 stream 64-byte UDP received
LAN on a port 17.6 Gbit/s 173 kpps
... with a flowtable 17.8 Gbit/s 295 kpps
LAN on a bridge 17.1 Gbit/s 107 kpps
... with a flowtable on its lower port 17.6 Gbit/s 293 kpps

What it says for the design is in interfaces.md ("Measured").

The image-mode pair (2026-10-20)

gw1 of the lab's active-active pair on the host image without functions (D23; stronghold:44.20261020.0049, stronghold 0.2.2, its functions' images 0.2.1), 2 vCPUs, 2 GiB, virtio: client1 on its LAN, an iperf3 server beyond its WAN (libvirt's default), NATed; 20 s runs (UDP 15 s), ping every 0.2 s beside each. The data and the report: out/perf/ in the workspace (perf.json).

WAN TCP up, 1 stream TCP down 64-byte UDP ping p50 / p95 under TCP load
shaped (CAKE, 20/50 Mbit), IDS preventing 19 Mbit/s 47 Mbit/s 19 kpps 0.7 / 1.1 ms
unshaped, IDS preventing (nfqueue) 0.64 Gbit/s 0.72 Gbit/s 149 kpps 44 / 62 ms
unshaped, IDS detecting 7.5 Gbit/s 19.9 Gbit/s 133 kpps 1.1 / 2.9 ms
  • The IDS in prevent mode is what limits a box: every WAN packet goes through Suricata (two vCPUs, ET Open's 53214 rules), about 0.65 Gbit/s either way, with the queue's latency; detecting, the box routes 7.5 Gbit/s up and 19.9 down. A box with prevent mode needs its cores for Suricata, but not all of them: on four vCPUs, Suricata on two of them (ids.cpus: 0-1) forwarded 3–4.4 Gbit/s up where on all four it did 0.6–0.8 (ids.md, "On four CPUs").
  • CAKE keeps latency where it was idle (0.9 ms) at the shaped rate; only a flood of UDP that no shaper refuses (64-byte packets, unlimited) reaches 65 ms at the 95th percentile.
  • DNS: the resolver's container answered 77,300 queries a second from its cache (0.68 ms on average, none lost) to one client of one vCPU: the client's limit, not the box's (the container at 2% CPU).
  • Failover (each three runs alike): a WAN's link lost costs 5.8 s of pings, its return none; a handover 0.4 s, a takeover 0.2 s; a node holding both groups losing its WAN, 0.4 s, and the peer's group going home after, 0.2 s (55 s before 0.2.3's fix).
  • Functions: installed in 2 s from the box's cache, 3–4.5 s from the registry (images of 210–265 MB); eight installed at the first start after the move to the image, 15 s.
  • Updates: 34 s (soft reboot) to 66 s (reboot) from stronghold update apply to the node back and healthy; a rollback a minute.
  • Footprint: strongholdd 21 MB; the containers 3–19 MB each, the resolver 160 MB (its blocklists), Suricata 580 MB; the host image 841 MB compressed. The API on its socket: 0.2–0.5 ms, /v1/functions 8 ms, /v1/firewall/counters 45 ms (it runs nft).