pathvector-dev

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 cyclic rotates 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)

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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 by run.sh.
  • nicolaka/netshoot:latest — the client, providing dig.

Running the Lab

The quick path, which builds, deploys, verifies, and tears down for you:

./scripts/labctl.sh run dnsrr-41

Enter fullscreen mode Exit fullscreen mode

Or step through it manually:

1. Move into the working directory

cd protocol-lab/examples/dnsrr-41

Enter fullscreen mode Exit fullscreen mode

2. Build the image and deploy

docker build -t protocol-lab/bind9:9.20 .
sudo containerlab deploy -t dnsrr-41.clab.yml

Enter fullscreen mode Exit fullscreen mode

3. Confirm the three A records

docker exec clab-dnsrr-41-client dig +noall +answer web.lab @10.0.0.2

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

The first address cycles: .11 → .12 → .13 → .11 → ….

Expected Output

  • dig +noall +answer: three A records for web.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/.13 is 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's rrset-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

Enter fullscreen mode Exit fullscreen mode

If you used labctl.sh run dnsrr-41, the script runs destroy for you at the end.

Check Your Understanding

  1. What is an RRset? Why can one name hold multiple A records?
  2. How does the client convention of using the first address make round-robin work?
  3. What does rrset-order cyclic do? Why does the first address cycle in this lab?
  4. Contrast DNS round-robin with anycast (Lab 31), ECMP (Lab 32), and IPVS (Lab 33) in terms of the layer where distribution happens.
  5. Name two weaknesses of DNS round-robin (health, caching).
  6. Why do round-robin A records get short TTLs? What's the downside of going too short?

References

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 by run.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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode


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.