Originally published at https://blog.pathvector.dev/protocol-lab-dnsrr-41/ — part of the free Protocol Lab series.
This post is part of Protocol Lab, a free, hands-on series for learning networking protocols by building and breaking them in a container lab. All the lab material — topologies, configs, and scripts — lives in the repo: github.com/pathvector-studio/protocol-lab.
The load-distribution labs so far worked in the network: anycast (Lab 31) let routing pick an instance, ECMP (Lab 32) hashed flows across links, and IPVS (Lab 33) had a director distribute connections. This lab spreads clients one layer earlier — at name resolution.
Reading guide: rfc-notes/dns-round-robin.md
Prerequisite: DNS Lab 05: Resolving a Name Through the Hierarchy
Expected time: 30–45 minutes.
The Goal
One name (web.lab.) holds three A records. With rrset-order cyclic, the authoritative server rotates their order on every response:
- a client that keeps re-resolving
web.lab.gets a different first address each time (.11 → .12 → .13 → …), - since clients typically connect to the first address returned, successive clients land on different backends,
- it's the simplest, cheapest spread — but the coarsest: no health checks, and caching blunts it.
By the end, you should be able to explain this table:
| query | first A record returned |
|---|---|
| 1 | 203.0.113.11 |
| 2 | 203.0.113.12 |
| 3 | 203.0.113.13 |
| 4 | 203.0.113.11 … |
What You Will Learn
- How one name can hold multiple A records (an RRset).
- Why clients using the first record makes the order matter.
- How
rrset-order cyclicrotates the RRset per response (round-robin). - Where DNS round-robin sits among anycast / ECMP / IPVS, and its trade-offs.
- Why TTL and caching limit how well it spreads.
This lab does not cover:
- Health-checked DNS load balancing (GSLB) or weighted records.
- GeoDNS / EDNS Client Subnet.
- Combining DNS with a real L4/L7 balancer (mentioned only in passing).
Where to Read in the RFCs
| Reference | What to focus on |
|---|---|
| RFC 1035 §3.4.1 | A records, RRsets |
| RFC 1794 | Load distribution via round-robin |
| RFC 8499 | The terms RRset / authoritative / resolver |
| RFC 5737 | Confirming the lab's 203.0.113.0/24 is documentation-only space |
The Big Picture
Two nodes: a client, and an authoritative DNS server for web.lab..
client (10.0.0.1) --- dns (10.0.0.2, authoritative for web.lab.)
dig web.lab @10.0.0.2 → A .11 / .12 / .13 (order rotates every response)
flowchart LR
C["client<br/>dig web.lab (×N)"] --> D["dns (authoritative)<br/>rrset-order cyclic"]
D -->|"query 1: .11, .12, .13"| C
D -->|"query 2: .12, .13, .11"| C
D -->|"query 3: .13, .11, .12"| C
10.0.0.0/24 is the lab link; 203.0.113.0/24 (RFC 5737) is the documentation space the A records point at.
Note: Everything here uses local/documentation address space (RFC 1918 / RFC 5737), so nothing in this lab touches the real internet.
What You Need
Recommended environment:
- Linux / WSL2 / a Linux VM
- Docker
- containerlab
Images used:
-
protocol-lab/bind9:9.20— built from the lab's Dockerfile byrun.sh. -
nicolaka/netshoot:latest— the client, providingdig.
Running the Lab
The quick path, which builds, deploys, verifies, and tears down for you:
./scripts/labctl.sh run dnsrr-41
Or step through it manually:
1. Move into the working directory
cd protocol-lab/examples/dnsrr-41
2. Build the image and deploy
docker build -t protocol-lab/bind9:9.20 .
sudo containerlab deploy -t dnsrr-41.clab.yml
3. Confirm the three A records
docker exec clab-dnsrr-41-client dig +noall +answer web.lab @10.0.0.2
web.lab. should show three records: A 203.0.113.11 / .12 / .13.
4. Query repeatedly and watch the first record rotate
for i in $(seq 1 6); do
docker exec clab-dnsrr-41-client sh -c 'dig +short web.lab @10.0.0.2 | head -1'
done
The first address cycles: .11 → .12 → .13 → .11 → ….
Expected Output
-
dig +noall +answer: three A records forweb.lab.. - The first address of successive queries cycles through
.11 → .12 → .13(cyclic). - Across six queries, all three addresses appear in first position.
Why It Works
DNS round-robin is "one name, multiple addresses, rotate the order on every response."
-
RRset. Multiple records with the same name and type form a single set (an RRset).
web.lab. A .11/.12/.13is three records, and a response normally returns all of them. - The first-record convention. Most clients and stub resolvers connect to the first address in the answer. That's why the order decides which backend each client hits.
-
Cycling with
rrset-order cyclic. The server rotates the RRset order on every response (BIND'srrset-order cyclic). Successive queries see the first address cycle.11 → .12 → .13, so successive clients land on different backends. The distribution happens at the name-resolution layer — a different layer from anycast / ECMP / IPVS, which work in routing and forwarding. - Coarseness and caching. DNS round-robin is the cheapest spread but also the coarsest. There are no health checks — a dead backend's A record keeps being handed out. And responses are cached for the TTL, during which the same order gets reused and the rotation has no effect. That's why round-robin A records get a short TTL (30 seconds in this lab). The distribution is best-effort.
- In production. DNS round-robin alone is a quick-and-dirty spread. Serious setups use GSLB (with health checks), or combine DNS round-robin with a real load balancer (Lab 33) or anycast (Lab 31).
The key insight: give one name several addresses and rotate the order, and clients get spread across the backend pool at the name-resolution stage. Cheap and easy — at the cost of being coarse.
Common Pitfalls
- Mistaking it for true load balancing. It's a coarse spread. It sees neither actual load nor connection counts.
- Assuming it checks health. Plain round-robin returns dead backends too. You need monitoring plus a low TTL plus record removal — or a real load balancer.
- Expecting an even split. Caching, resolver implementations, and client address selection all skew it.
- Ignoring TTL. A long TTL lets caches kill the rotation. Round-robin wants short TTLs.
- Assuming non-first records get used. Most clients only use the first — which is exactly why the order matters.
- Putting a recursive resolver in the middle. The resolver caches and may reorder answers, so the rotation can be much less visible than when querying the authoritative server directly.
Cleanup
sudo containerlab destroy -t dnsrr-41.clab.yml --cleanup
If you used labctl.sh run dnsrr-41, the script runs destroy for you at the end.
Check Your Understanding
- What is an RRset? Why can one name hold multiple A records?
- How does the client convention of using the first address make round-robin work?
- What does
rrset-order cyclicdo? Why does the first address cycle in this lab? - Contrast DNS round-robin with anycast (Lab 31), ECMP (Lab 32), and IPVS (Lab 33) in terms of the layer where distribution happens.
- Name two weaknesses of DNS round-robin (health, caching).
- Why do round-robin A records get short TTLs? What's the downside of going too short?
References
- RFC 1035: Domain Names — Implementation and Specification
- RFC 1794: DNS Support for Load Balancing
- RFC 8499: DNS Terminology
- RFC 5737: IPv4 Address Blocks Reserved for Documentation
Verified Run Log (2026-07-08)
This lab has been confirmed reproducible on real hardware.
Environment:
- Ubuntu 26.04 LTS (kernel 7.0.0-27-generic, x86_64)
- Docker 29.1.3
- containerlab 0.77.0
- dns:
protocol-lab/bind9:9.20(built from the Dockerfile byrun.sh) - client:
nicolaka/netshoot:latest(dig)
Running PATH="/tmp/pl-shim:$PATH" ./scripts/labctl.sh run dnsrr-41 performed build → deploy → verify → destroy, and verification.json returned "status": "verified".
One name, three addresses: the RRset
web.lab. 30 IN A 203.0.113.11
web.lab. 30 IN A 203.0.113.12
web.lab. 30 IN A 203.0.113.13
web.lab. holds three A records (an RRset) with a TTL of 30 seconds — kept short so caches expire quickly and clients re-resolve and re-spread.
The first address cycles on every response
Six runs of dig +short web.lab @10.0.0.2 | head -1 (first address only):
203.0.113.12 → 203.0.113.13 → 203.0.113.11 → 203.0.113.12 → 203.0.113.13 → 203.0.113.11
Thanks to rrset-order cyclic, the authoritative server rotates the RRset order on every response. Across six queries, all three addresses appear in first position (distinct = 3), confirming that clients get roughly spread across the three backends. This is distribution at the name-resolution layer, not the network layer (anycast / ECMP / IPVS).
Cleanup
containerlab destroy -t dnsrr-41.clab.yml --cleanup
That's DNS round-robin: three A records, a rotating answer, and clients spreading themselves across your backends before a single packet of application traffic flows. The cheapest load distribution you can deploy — as long as you respect its blind spots.
Explore the full Protocol Lab series here: github.com/pathvector-studio/protocol-lab. If these labs are useful to you, please ⭐ star the repo on GitHub — it genuinely helps others find the project.
Next up, we'll build on this naming-layer view and look at what it takes to make DNS-based distribution health-aware.
Top comments (0)