# keepalived check that fails over when Pi-hole stops blocking, not just when it stops answering

**URL:** <https://discourse.pi-hole.net/t/keepalived-check-that-fails-over-when-pi-hole-stops-blocking-not-just-when-it-stops-answering/87803>\
**Category:** Community How-to's\
**Created:** [September 29, 2026, 11:51pm UTC](https://discourse.pi-hole.net/t/keepalived-check-that-fails-over-when-pi-hole-stops-blocking-not-just-when-it-stops-answering/87803 "2026-09-29T23:51:17Z")\
**Posts on this page:** 5\
**Page:** 1

<div class="post-metadata">

**Author:** ![sysdad](https://discourse-cdn.pi-hole.net/user_avatar/discourse.pi-hole.net/sysdad/32/56403_2.png) [@sysdad](https://discourse.pi-hole.net/u/sysdad)\
**Post date:** [September 29, 2026, 11:51pm UTC](https://discourse.pi-hole.net/t/keepalived-check-that-fails-over-when-pi-hole-stops-blocking-not-just-when-it-stops-answering/87803/1 "2026-09-29T23:51:17Z")

</div>

If you run two Pi-holes behind a keepalived VIP, your health check probably asks "does DNS answer?" Mine did. It catches a dead Pi-hole. It doesn't catch the one that's up, answering everything, and blocking nothing.

That's what happened to me recently. A sync tool (nebula-sync with `RUN_GRAVITY=true`) started twice in quick succession and ran two gravity rebuilds on my primary at once. The primary came out with no `gravity` table and every group mapping empty. DNS worked fine. Blocking was off for the whole house, and keepalived had no reason to move the VIP to the perfectly healthy backup.

So I wrote a second track script. It asks the node for `doubleclick.net` and fails only if it gets a real IP **while blocking is switched on**. That second part matters. Pausing Pi-hole sets `dns.blocking.active` to `false`, so a pause never moves the VIP. A broken database leaves it `true`.

Tested on v6 (FTL 6.7.1) with keepalived 2.2.7:

- forced failure on the primary: VIP moved in 28 s and came back 48 s after clearing, with no failed queries
- real mapping wipe on the backup: its priority dropped, VIP stayed put
- 5-minute pause of both nodes: nothing happened, which is the point

One trap if you write your own: don't use `+norecurse` for this. It's fine for a `pi.hole` liveness check, but an unblocked name is then answered only from cache, and the check flips between pass and fail without ever tripping.

Script, keepalived config and notes: [Pi-hole + keepalived: fail over when Pi-hole stops blocking, not just when it stops answering · GitHub](https://gist.github.com/sysdad/61fe3be3db3c360f920283254cf3072e)

Two related lessons, in case they save someone an evening of tinkering:

- I've turned `RUN_GRAVITY` off. Same fix as the "Nightly Failures of DB causing outages" thread posted previously.
- nebula-sync also syncs on every container start, so a Docker upgrade or reboot is a sync. I've opened an issue asking for a way to turn that off: [Every container start runs a full sync with RUN\_GRAVITY, and gravity runs on the primary too. Two quick restarts wiped my primary. · Issue #271 · lovelaze/nebula-sync · GitHub](https://github.com/lovelaze/nebula-sync/issues/271)

As-is, not a maintained project. Happy to answer questions here.

---

<div class="post-metadata">

**Author:** ![nero355](https://discourse-cdn.pi-hole.net/user_avatar/discourse.pi-hole.net/nero355/32/54774_2.png) [@nero355](https://discourse.pi-hole.net/u/nero355)\
**Post date:** [September 30, 2026, 4:31pm UTC](https://discourse.pi-hole.net/t/keepalived-check-that-fails-over-when-pi-hole-stops-blocking-not-just-when-it-stops-answering/87803/2 "2026-09-30T16:31:24Z")

</div>

Time to upgrade : [[RELEASE] Pi-hole HA — automatic DHCP failover, VIP, and config sync for a Pi-hole cluster](https://discourse.pi-hole.net/t/release-pi-hole-ha-automatic-dhcp-failover-vip-and-config-sync-for-a-pi-hole-cluster/86667)

And mention anything you are missing to @RamSet 😬👍👍

---

<div class="post-metadata">

**Author:** ![sysdad](https://discourse-cdn.pi-hole.net/user_avatar/discourse.pi-hole.net/sysdad/32/56403_2.png) [@sysdad](https://discourse.pi-hole.net/u/sysdad)\
**Post date:** [September 30, 2026, 5:14pm UTC](https://discourse.pi-hole.net/t/keepalived-check-that-fails-over-when-pi-hole-stops-blocking-not-just-when-it-stops-answering/87803/3 "2026-09-30T17:14:47Z")

</div>

Thanks! I hadn't seen Pi-hole HA. I read through it, and it's a cleaner setup than keepalived plus nebula-sync. It also copies `gravity.db` instead of triggering gravity remotely, which sidesteps the double-rebuild that wiped my primary.

Two things would still have bitten me with it: the health check doesn't test blocking, and the sync doesn't check what's in the database before copying it. I've passed both along to RamSet in his release thread. If they land, I'd move my cluster over.

---

<div class="post-metadata">

**Author:** ![nite\_route](https://discourse-cdn.pi-hole.net/user_avatar/discourse.pi-hole.net/nite_route/32/56186_2.png) [@nite\_route](https://discourse.pi-hole.net/u/nite_route)\
**Post date:** [October 2, 2026, 4:13pm UTC](https://discourse.pi-hole.net/t/keepalived-check-that-fails-over-when-pi-hole-stops-blocking-not-just-when-it-stops-answering/87803/4 "2026-10-02T16:13:02Z")

</div>

yeah dig-for-answer only misses a live pihole that stopped blocking.

i run a keepalived check that digs a known blocked domain and greps the sinkhole:

```auto
#!/bin/bash
IP=$(dig +short +time=2 +tries=1 doubleclick.net @127.0.0.1 | head -1)
[-z "$IP"] && exit 1
case "$IP" in
  0.0.0.0|127.0.0.1|::|::1) exit 0 ;;
  *) exit 1 ;;
esac

```

point track\_script at that. VIP drops when blocking dies. pick a domain u know is on ur gravity (and tweak the case if ur mode returns NXDOMAIN instead).

---

<div class="post-metadata">

**Author:** ![sysdad](https://discourse-cdn.pi-hole.net/user_avatar/discourse.pi-hole.net/sysdad/32/56403_2.png) [@sysdad](https://discourse.pi-hole.net/u/sysdad)\
**Post date:** [October 3, 2026, 4:32am UTC](https://discourse.pi-hole.net/t/keepalived-check-that-fails-over-when-pi-hole-stops-blocking-not-just-when-it-stops-answering/87803/5 "2026-10-03T04:32:40Z")

</div>

Thanks - that's the shape I started with. The catch I hit: a deliberate `pihole disable` also returns a real IP, so the VIP moves on every pause, and if you pause both nodes together (I do, from Home Assistant) both fail the check and keepalived drops the VIP entirely - a DNS outage instead of a few ads. That's why mine reads `dns.blocking.active` first and only fails on a real answer while blocking is supposed to be on. Agreed on picking a domain you know is in gravity, and on adjusting for NXDOMAIN mode.
