BGP performance testing with Ze
A route-server performance test is simple in outline: create a route source, connect it to a device under test, watch a monitor peer, and publish how many routes arrived and how long convergence took.
AMS-IX used ExaBGP as a lightweight BGP route source. bgperf formalized the same idea with tester, target, and monitor containers. Ze can run that shape with ze-perf: Ze supplies the sender and receiver, the target is whatever BGP speaker you want to test, and the output is a repeatable report instead of a stopwatch and screenshots.
This page shows Ze in the tester role. The DUT can be FRR, BIRD, GoBGP, OpenBGPD, a route-server appliance, or Ze itself.
What a good test answers
A performance test has to answer these questions, and publish the evidence for each.
| Question | Evidence to publish |
|---|---|
| Can the DUT establish the sessions? | Sender and receiver BGP sessions reach Established |
| Can the DUT ingest the route set? | Expected route count arrives at the receiver |
| Did it lose routes? | routes-lost is zero |
| How long did convergence take? | convergence-ms, throughput, and latency percentiles from ze-perf |
| Does the result repeat? | Several iterations, warmups discarded, standard deviation reported |
| Can someone reproduce it? | DUT config, route count, family, batch size, hardware, runtime, and raw JSON |
A good run proves a complete path: generated routes enter the DUT, pass through the DUT policy and RIB path, leave the DUT, and arrive at a real BGP receiver. A route count alone is wiring. A repeated run with zero loss and published inputs is evidence.
Topology
The test is three nodes: a sender and a receiver that Ze runs, with the device under test between them.
ze-perf sender ---> DUT under test ---> ze-perf receiver
AS 65001 AS 65000 AS 65002
| Node | AS | Played by | Job |
|---|---|---|---|
| Sender | 65001 | ze-perf |
Generate the route set and inject it into the DUT |
| Device under test | 65000 | FRR, BIRD, GoBGP, OpenBGPD, Ze, or another BGP speaker | Receive the routes, apply policy, and re-advertise them |
| Receiver | 65002 | ze-perf |
Record when each prefix arrives |
ze-perf matches the prefixes the sender injected against the ones the receiver saw, so it can report convergence, throughput, latency percentiles, withdrawal time, and lost routes. In bgperf terms, the sender is the tester, the DUT is the target, and the receiver is the monitor.
Fastest working test
Use the existing Docker runner from the main Ze checkout when you want the whole setup built for you.
python3 test/perf/run.py --build --test frr
ze-perf report test/perf/results/*.json
Run a smaller wiring test first when you are adding a new DUT target.
ze-perf run \
--dut-addr 172.31.0.2 \
--dut-asn 65000 \
--dut-name frr \
--routes 10 \
--repeat 1 \
--warmup-runs 0
If 10 routes do not converge, fix the BGP sessions or DUT policy before running a scale test.
A 100k route run
This is the Ze equivalent of using ExaBGP as the injector in the original route-server tests.
ze-perf run \
--dut-addr 172.31.0.2 \
--dut-asn 65000 \
--dut-name frr \
--sender-addr 172.31.0.10 \
--sender-asn 65001 \
--receiver-addr 172.31.0.11 \
--receiver-asn 65002 \
--routes 100000 \
--repeat 10 \
--warmup-runs 2 \
--iter-delay 3s \
--output results/frr-100k.json
The sender and receiver addresses must be valid local addresses in the lab where ze-perf runs. The Docker runner handles that wiring. In a manual lab, bind those addresses on the host or run ze-perf in the right network namespace.
Then render the report.
ze-perf report results/frr-100k.json
Use --family ipv6/unicast for IPv6. Use --force-mp to encode IPv4 NLRI through MP_REACH_NLRI and test the DUT multiprotocol path. Use --batch-size 1 when you want one prefix per UPDATE instead of packed UPDATEs.
FRR as the DUT
This FRR config accepts the Ze sender and exports the received routes to the Ze receiver.
frr defaults traditional
hostname dut-frr
log stdout informational
!
route-map ACCEPT permit 10
!
router bgp 65000
bgp router-id 172.31.0.2
no bgp ebgp-requires-policy
neighbor 172.31.0.10 remote-as 65001
neighbor 172.31.0.11 remote-as 65002
!
address-family ipv4 unicast
neighbor 172.31.0.10 activate
neighbor 172.31.0.10 route-map ACCEPT in
neighbor 172.31.0.11 activate
neighbor 172.31.0.11 route-map ACCEPT out
exit-address-family
!
Useful checks while debugging:
show bgp ipv4 unicast summary
show bgp ipv4 unicast 10.0.0.0/24
PfxRcd on the sender neighbor should reach the generated route count. The receiver side should receive the same count.
BIRD as the DUT
This BIRD 2 config imports routes from the Ze sender and exports them to the Ze receiver.
log stderr all;
router id 172.31.0.2;
protocol device {
}
protocol bgp ze_sender {
local 172.31.0.2 as 65000;
neighbor 172.31.0.10 as 65001;
ipv4 {
import all;
export none;
};
}
protocol bgp ze_receiver {
local 172.31.0.2 as 65000;
neighbor 172.31.0.11 as 65002;
ipv4 {
import none;
export all;
};
}
Useful checks:
birdc show protocols
birdc show route count
What Ze reports
ze-perf reports the fields that are easy to lose when the test is driven by an external injector.
| Field | Why it matters |
|---|---|
convergence-ms |
Time from the sender writing the first UPDATE to the receiver collecting the expected routes |
throughput-rps |
Sustained route propagation rate over the convergence window |
latency-p50-ms, latency-p90-ms, latency-p99-ms, latency-max-ms |
Per-route propagation distribution |
withdrawal-ms |
Time for the receiver to become idle after withdrawal |
routes-lost |
Route loss, which invalidates a throughput claim until fixed |
repeat, warmup-runs, repeat-kept |
How much measurement stability the run has |
seed, family, force-mp, batch-size |
Inputs needed to reproduce the route stream |
Use JSON for anything you intend to publish.
ze-perf run --dut-addr 172.31.0.2 --dut-asn 65000 --routes 100000 --output result.json
What to publish with the number
Publish the method with the result.
| Field | Why it matters |
|---|---|
| DUT name, version, commit, and config | Policy and defaults change results |
| Hardware or VM shape | CPU, memory, and virtualization dominate control-plane tests |
| Runtime | Bare metal, Docker, Colima, QEMU, and VM networking are different tests |
| Route source | Synthetic routes, MRT replay, and production captures stress different code paths |
| Peer shape | One large peer and many small peers find different bugs |
| Route count and family | IPv4 inline NLRI, IPv4 MP_REACH, and IPv6 MP_REACH exercise different paths |
| Batch size | Packed UPDATEs and one prefix per UPDATE are different workloads |
| Repeats and warmups | One run is a smoke test |
| Lost routes | Loss must be explained before throughput matters |
| Raw JSON | The number can be checked later |
How close Ze is to the full route-server test
Ze already covers the core of the test: sender, DUT, receiver, route count, convergence, loss, and repeatable output.
The current ze-perf shape is one sender session and one receiver session. That is enough to test route propagation and DUT table pressure. It is not the same as hundreds of independent BGP client sessions in one run.
If the target is the full AMS-IX or bgperf style route-server scale test, the useful Ze feature would be a multi-peer mode:
| Missing shape | Feature to add |
|---|---|
| 500 small route-server clients | N sender sessions, each with its own local address, ASN, and route slice |
| MRT replay as route source | Route input from MRT, with count and skip controls |
| External generator with Ze monitor | Receiver-only mode that records first route, last route, loss, and receive window |
| Per-client reporting | JSON grouped by sender peer and route family |
That would let Ze reproduce the full route-server topology while keeping Ze's timing and JSON reporting.
Prior art
AMS-IX published route-server performance slides at RIPE64 with 500 IPv4 peers, 128,000 prefixes, BIRD table-mode comparisons, an MTU test with 1012 IPv6 peers, and a note to check out ExaBGP as a lightweight tool to open BGP sessions and inject prefixes: AMS-IX Route-Server Performance Test.
Job Snijders' bgp-performance repository is a bgperf2-based performance suite. Its docs describe a target, monitor, and tester topology, and its MRT mode can use ExaBGP as an MRT injector: job/bgp-performance, how bgperf works, and MRT injection.
Lab evidence
Ze's automated performance evidence already wires sender, DUT, and receiver the same way.
| File | What it proves |
|---|---|
internal/perf/cli/cmd_run.go |
ze-perf run accepts DUT, sender, receiver, repeat, warmup, JSON, output, family, MP, and batch flags |
internal/perf/result.go |
JSON results include convergence, throughput, latency percentiles, withdrawal time, lost routes, seed, family, and repeat metadata |
test/perf/run.py |
The Docker runner benchmarks Ze, FRR, BIRD, GoBGP, rustbgpd, RustyBGP, freeRtr, and OpenBGPD |
test/perf/configs/ |
DUT configs are stored with the test runner rather than hidden in a report |
Run the Ze performance suite from the main checkout:
python3 test/perf/run.py --build --test frr bird gobgp
ze-perf report test/perf/results/*.json
The full benchmarking guide is at docs/guide/benchmarking.