Skip to content
Networking

Trying to load-balance DNS across three Pi-holes (and settling for failover)

By Victor Da Luz
RouterOS MikroTik Pi-hole DNS networking homelab failover
Trying to load-balance DNS across three Pi-holes (and settling for failover)

I run three Pi-hole instances across my VLANs, kept in sync so any of them can answer any query: identical blocklists, identical local records. The idea was to spread DNS load across all three and get redundancy as a bonus. Three resolvers sharing the work.

Then I actually looked at the query counts. pihole01 was handling around four queries a second. pihole02 saw maybe ten queries a minute. pihole03 was effectively idle, answering the odd monitoring probe and little else. Roughly 99.9% of my DNS was hitting a single box, and the other two were expensive standbys.

This post is the rabbit hole I went down trying to even that out, and the unsatisfying but correct place I ended up.

The setup that skews

Every client gets the router’s gateway IP as its DNS server over DHCP. The clients talk to RouterOS, and RouterOS forwards to the Pi-holes. So the question was simple: given a list of three Pi-holes, how do I get RouterOS to spread queries across them?

The first attempt was the obvious one. Hand RouterOS all three and let it sort them out:

/ip dns set allow-remote-requests=yes
/ip dns set servers=192.168.20.12,192.168.20.13,192.168.20.14

No change. pihole01 still took everything. RouterOS treats that list as a failover order, not a rotation. It tries the first server, and once it gets an answer, it keeps using that one.

Forwarders, then a hostname trick

The next idea was DNS forwarders, which I’d seen described as supporting real round-robin:

/ip dns forwarders add name=pihole-forwarder dns-servers=192.168.20.12,192.168.20.13,192.168.20.14
/ip dns static add name="." type=FWD forward-to=pihole-forwarder

Same result. pihole03 still got nothing. So I tried to force the issue with a local hostname that resolved to all three Pi-hole IPs, hoping RouterOS would round-robin between the A records:

/ip dns static add name=pihole-resolver.internal address=192.168.20.12
/ip dns static add name=pihole-resolver.internal address=192.168.20.13
/ip dns static add name=pihole-resolver.internal address=192.168.20.14
/ip dns forwarders add name=pihole-forwarder dns-servers=pihole-resolver.internal

I measured the result over a few minutes of normal traffic:

  • pihole01: ~4 queries/second
  • pihole02: ~10 queries/minute
  • pihole03: ~2 queries/minute

Better than zero on pihole02, but nowhere near even. RouterOS was still latching onto one upstream and caching aggressively.

The distinction that explains everything

At this point I reached for the tool that had worked before. In a previous post about preventing DNS bypass, I spread redirected external DNS across these same three Pi-holes using nth round-robin in a dstnat rule, and the distribution evened out immediately. So I built the equivalent here with PCC and NAT marks.

It didn’t fire at all. That’s when the actual problem clicked.

The bypass-prevention rules work on traffic the router forwards: a device queries 8.8.8.8, the packet heads through the router toward the internet, and a dstnat rule matching external destinations redirects it to a Pi-hole. But here the clients aren’t aiming at an external resolver. They query the router’s own IP, because that’s the DNS server DHCP handed them. Those redirect rules only matched DNS bound for somewhere other than my Pi-holes, so a query already addressed to the router never matched them in the first place. And the thing that actually chooses a Pi-hole here isn’t NAT at all. It’s the router’s own resolver, forwarding to its server list, with no nth counter anywhere in that path. After the router resolves the name, the response comes back through the INPUT chain, not the forward path the redirect rules live on.

That distinction is why my earlier nth trick has nothing to say here. It can balance DNS the router relays. It can’t balance DNS the router answers.

So why does RouterOS’s own resolver stick to pihole01? The MikroTik DNS documentation spells it out:

Servers are processed in a queue order - static servers as an ordered list, dynamic servers as an ordered list. When DNS cache has to send a request to the server, it tries servers one by one until one of them responds. After that this server is used for all types of DNS requests.

That’s the whole story. RouterOS DNS is failover, not load balancing, by design. Once pihole01 answers, every subsequent query goes to pihole01 until it stops answering. There’s no per-query rotation, and no NAT rule can bolt one on for queries the router resolves itself.

The honest answer, and why I didn’t take it

There is a way to balance these queries: stop pointing clients at the router and hand each client all three Pi-hole IPs directly over DHCP. Then the clients distribute themselves, the way DNS clients normally do.

I tried it. It broke things. Devices holding old leases kept querying the router. New-lease devices that now talked to Pi-hole directly hit failures whenever a Pi-hole was mid-restart, and a stray DHCP Option 6 was overriding the DNS field I thought I was setting. For a change whose only upside was prettier query graphs, the blast radius wasn’t worth it. I reverted.

I did come back to client-side distribution much later, and made it stick by giving each VLAN its own local Pi-hole as the primary with a couple of shared replicas behind it. Handing every client the same three IPs was the version that broke; per-VLAN local primaries were the version that held. That’s a later story. For now, I went back to the router.

What I settled on

Failover, deliberately, with a real escape hatch:

/ip dns set allow-remote-requests=yes \
  servers=192.168.20.12,192.168.20.13,192.168.20.14,1.1.1.1,1.0.0.1 \
  query-server-timeout=3s query-total-timeout=10s

Clients still point at the router. RouterOS prefers pihole01, falls through to pihole02 then pihole03 if one stops answering, and only as a last resort reaches Cloudflare at 1.1.1.1 and 1.0.0.1. Because all three Pi-holes are kept identical by sync, any of them can serve the full blocklist. The Cloudflare entries mean a total Pi-hole outage costs me ad-blocking, not the internet.

pihole01 still does almost all the work. I’m fine with that now. A single Pi-hole handles my entire network’s query volume without breaking a sweat, so “even distribution” was solving a problem I didn’t actually have.

Lessons

  • RouterOS DNS is failover, not load balancing. A multi-server list, FWD forwarders, and multiple A records all collapse to “use the first one that answers.”
  • Which firewall chain matters. NAT and nth only touch traffic the router forwards (dstnat/prerouting). Queries aimed at the router itself go through INPUT, where NAT redirection never runs.
  • Client-side balancing is the only real option, and it isn’t free. Handing clients multiple resolvers shifts the work to them, along with lease churn and per-client DNS quirks.
  • Redundancy and load balancing aren’t the same goal. I wanted balancing and actually needed redundancy. Failover plus an upstream fallback gave me the resilience; the balancing was vanity.
  • Measure before chasing. One Pi-hole was comfortably handling the whole load. The skew looked like a problem mostly because I went looking at the graph.

Related reading

Ready to Transform Your Career?

Let's work together to unlock your potential and achieve your professional goals.