Running your own DNS resolver is one of the highest-leverage things in a home network, and one of the most fragile. When Pi-hole or AdGuard Home stops answering, nothing says "DNS is down." Every phone, TV, and laptop says the internet is down, the smart speakers stop responding, and the dashboard that would show you the problem is at http://pi.hole/admin, a name that no longer resolves. You find out from the household, and you find out all at once.

The resolver is also the one service an outside monitor is least able to see. DNS is UDP, so a TCP port check on 53 has nothing to shake hands with, and a home resolver should never be reachable from the internet to begin with. So this guide monitors it the only way that works: from inside, with a short script that asks the resolver real questions every minute and pings a heartbeat only when the answers are right.

How a resolver fails without going down

  • The service died. pihole-FTL or AdGuardHome stopped: an update failed, the SD card went read-only, the Pi rebooted and the service didn't come up. Total outage, loudest failure.
  • It answers but can't resolve. The upstream (Cloudflare, Quad9, your ISP, a local Unbound) is unreachable or your DNS-over-HTTPS proxy crashed. Cached names work for a while, so the outage looks intermittent and gets blamed on Wi-Fi.
  • It resolves but doesn't block. Blocking was disabled "for 5 minutes" and never re-enabled, or gravity failed to update and the blocklist is weeks stale. No one notices until the ads come back.
  • It's fine, and nobody's using it. The router's DHCP was reset and hands out the ISP's resolver again. The Pi-hole is healthy and idle. Only a check from a client catches this one.
  • The Pi is gone. Power, SD card, network. Also takes down DHCP if the Pi-hole serves it, which turns a DNS outage into a "nothing gets an address" outage after leases expire.

The heartbeat script

Create a heartbeat monitor in CronAlert with a 1-minute expected interval; with the default grace it alerts after two minutes of silence. Then install this on the Pi (it works unchanged on a Pi-hole in Docker if you run it on the host and point RESOLVER at the container's address):

#!/usr/bin/env bash
# /usr/local/bin/dns-health.sh — run from cron every minute
set -uo pipefail
HEARTBEAT="https://cronalert.com/api/heartbeat/<token>"
RESOLVER=127.0.0.1                 # the Pi-hole / AdGuard Home address
SERVICE=pihole-FTL                 # AdGuardHome for AdGuard Home
GRAVITY=/etc/pihole/gravity.db     # Pi-hole only; leave empty for AdGuard Home
fail=0

# 1. The service is running
systemctl is-active --quiet "$SERVICE" || { echo "$SERVICE not active"; fail=1; }

# 2. A real name resolves through the local resolver (tests the upstream path too)
dig @"$RESOLVER" +short +time=2 +tries=1 "probe-$(date +%s).cronalert.com" A >/dev/null 2>&1 \
  && dig @"$RESOLVER" +short +time=2 +tries=1 example.com A | grep -qE '^[0-9]+\.' \
  || { echo "resolution failed"; fail=1; }

# 3. Blocking is active: a known ad domain returns the block address (0.0.0.0 or ::) 
blocked=$(dig @"$RESOLVER" +short +time=2 +tries=1 doubleclick.net A)
echo "$blocked" | grep -qE '^(0\.0\.0\.0|::)?$' || { echo "blocking not active (doubleclick.net -> $blocked)"; fail=1; }

# 4. Pi-hole only: blocklists refreshed within 8 days (weekly gravity update)
if [ -n "$GRAVITY" ] && [ -f "$GRAVITY" ]; then
  [ -n "$(find "$GRAVITY" -mtime -8)" ] || { echo "gravity.db older than 8 days"; fail=1; }
fi

# 5. Disk and SD card health: root filesystem writable and below 90%
touch /var/tmp/.dns-health-write 2>/dev/null || { echo "root filesystem not writable"; fail=1; }
use=$(df --output=pcent / | tail -1 | tr -dc '0-9')
[ "$use" -ge 90 ] && { echo "root disk ${use}%"; fail=1; }

# Ping only when everything passed; silence is the alert
[ "$fail" -eq 0 ] && curl -fsS -m 10 -X POST "$HEARTBEAT" >/dev/null
exit 0
# /etc/cron.d/dns-health
* * * * * root /usr/local/bin/dns-health.sh 2>&1 | logger -t dns-health

Notes on the checks. The resolution test queries a unique, never-cached name first so the upstream path is exercised on every run (a cached example.com would pass for hours after the upstream died), then confirms a stable name returns an address. The blocking test expects Pi-hole's and AdGuard Home's default null-blocking behavior, where blocked names return 0.0.0.0 or an empty answer; if you've configured a different blocking mode, adjust the pattern. The script deliberately avoids the web API, which Pi-hole rewrote in v6 (the old /admin/api.php is gone) and which requires authentication on AdGuard Home; dig, systemctl, and the filesystem behave the same on every version. On a Pi-hole, pihole status is a fine extra line if you want its view too. When the heartbeat goes silent, journalctl -t dns-health names the check that failed.

Why the script still works when it's the DNS that broke

There's an obvious objection: the script pings cronalert.com, and if the Pi-hole is the Pi's own resolver and it's down, the ping can't resolve either. That's true, and with a heartbeat it doesn't matter. A failed check and a failed ping produce the same thing: silence, which is the alert. This is the one monitoring pattern where the monitoring path breaking in the same outage still gives the right answer, and it's why the resolver gets a heartbeat rather than anything that would need to report a failure actively.

If you'd like the script to keep reporting through a resolver outage so the journal line is more specific, either give the Pi a second nameserver in its own /etc/resolv.conf (or systemd-resolved fallback) that isn't the Pi-hole, or run the script from another LAN machine with RESOLVER set to the Pi-hole's IP and its own DNS pointed elsewhere. That second placement also catches the fourth failure above: run dig example.com without @ from a client and compare the server in the answer to the Pi-hole's address, and you'll know the moment the router stops handing out your resolver.

Two resolvers, two heartbeats

The standard advice for home DNS is to run two resolvers (two Pi-holes, or Pi-hole plus AdGuard Home, or one of each on different hardware) so a single failure doesn't take the house down. Monitor them separately: one heartbeat each, named for the device. With one resolver, an alert means the internet is down for everyone and you fix it now. With two, an alert on one means you have redundancy left and a repair to schedule, which is the whole point of having two. If you sync them with Gravity Sync or similar, the gravity-freshness check on the secondary also tells you when the sync stopped.

If you expose DNS over HTTPS

Some setups publish the resolver on a real hostname for DNS-over-HTTPS, so phones use it away from home. That path is HTTPS, and an external HTTP monitor can see it: a GET on https://dns.example.com/dns-query?dns=… with a base64url-encoded query is the standard form, but a simpler liveness check is any request to the DoH hostname expecting the status your server returns to a bare GET (often 400), which proves the TLS, the proxy, and the listener are up and tracks the certificate. That check is on the free plan. The heartbeat still covers resolution and blocking from inside.

Frequently asked questions

Can CronAlert check my Pi-hole from outside?

No; DNS is UDP and a home resolver shouldn't be internet-facing. The heartbeat script from inside covers it.

Won't the script fail to reach CronAlert when DNS is down?

Probably, and that's the alert. Silence from a failed ping and silence from a failed check are the same signal.

Pi-hole v6?

Yes. The script uses dig, systemctl, and the gravity file, not the web API that changed in v6.

AdGuard Home?

Set the service name to AdGuardHome and leave the gravity path empty; everything else is identical.

Which plan?

Heartbeats are Pro ($5/mo). An external check on a DoH hostname is free.

The service that announces every other outage needs its own

DNS is the one dependency that makes every other failure look like itself, so it deserves a monitor that doesn't depend on it. A one-minute script asking the resolver five real questions, and a heartbeat that goes quiet when any answer is wrong, gets you a specific alert before the first "is the internet down?" Create an account, upgrade to Pro, and give each resolver its heartbeat. Related reading: DNS monitoring for the public side of the same problem, monitoring Home Assistant, monitoring a NAS, monitoring a VPS, and choosing between HTTP, TCP, and heartbeat monitors.