I replaced fourteen overlapping Pi-hole blocklists with one maintained list, but only after replaying a week of my own DNS traffic against each candidate.
A Reddit post made the case for merging every blocklist you use into one “master” list, hosting it on GitHub, and pointing Pi-hole at that instead.
It was tempting. My Pi-hole had fourteen adlists, most of them hosts files from around 2020, and the overlap between them was obvious just from the names.
Then I thought about what a merged list would actually buy me. Gravity, the Pi-hole process that builds the blocking database, already deduplicates across lists. My two Pi-holes are already kept identical by nebula-sync, so I already have one source of truth. A self-hosted master list would add a pipeline I have to maintain, and it would hide the one thing the Pi-hole UI tells you about each list: whether it is still updating.
The problem wasn’t the number of lists. It was that I didn’t know which of them were doing any work.
The hypothesis
One actively maintained list will block at least as much of my real traffic as fourteen stale ones, and nothing breaks along the way.
The first check was how much each list contributed on its own. Two were nearly dead weight:
| List | Domains no other list blocks |
|---|---|
| AdAway | 18 |
| UncheckyAds | 0 |
UncheckyAds was 9 domains in total, every one of them already covered elsewhere. The rest weren’t much better. They overlapped heavily and were barely maintained.
That told me the old stack was bloated. It didn’t tell me whether a replacement would be better, so I tested that separately.
Test one: replay a week of real queries
Blocklist comparisons usually count domains: list A has 400k, list B has 200k, so A must be better. That number means nothing for my network. What matters is which of the domains my devices actually ask for get blocked.
So I took seven days of query history from the Pi-hole’s own database. In that week, gravity blocked 355 distinct domains across about 71,600 queries. I rebuilt the blocking decision for every query against each candidate list, and kept my own allow, deny and regex rules plus the three niche lists I wasn’t replacing (an anti-malware list and two LG TV telemetry lists).
| Candidate | Domains still blocked | Share of blocked queries kept | Domains no longer blocked | Newly blocked |
|---|---|---|---|---|
| HaGeZi Multi PRO | 309 | 93% | 46 (4.9k queries) | 503 domains (12.3k queries) |
| OISD Big | 244 | 78% | 111 (16k queries) | 171 domains (2.7k queries) |
OISD Big is the other list that comes up in every recommendation thread. On my traffic it lost 16,000 blocked queries a week and picked up only 2,700 new ones. HaGeZi Multi PRO kept 93% of what was being blocked and added 12,300 queries’ worth of new blocks. On real traffic it wasn’t close.
I also tested HaGeZi PRO plus its threat-intelligence list (TIF). In the replay it scored the same as PRO alone, and that result is expected. A threat-intel list exists for domains you haven’t visited yet: new phishing hosts, fresh malware infrastructure. A replay of past traffic can’t measure that by design. I left TIF out for now. It’s a decision about how much risk I accept, and a replay can’t answer it.
Test two: a live A/B between two resolvers
A replay tells you what would have been blocked. It doesn’t tell you whether a banking app silently fails its login check. For that I needed real devices on the new list, with a way to roll back.
I run two Pi-holes. The primary feeds the secondary through nebula-sync every 15 minutes. For the trial I:
- stopped the sync timer, so the secondary kept the old lists as a control
- on the primary only, disabled eleven of the fourteen lists rather than deleting them, each tagged with the date and the reason
- added HaGeZi PRO in its adblock format, which Pi-hole v6 handles natively: one entry blocks a domain and all of its subdomains
- backed up
gravity.dbbefore touching anything
Gravity went from 210,346 to 239,612 unique domains. The container used 215 MB of its 2 GB.
Querying both resolvers side by side showed the difference immediately:
| Effect | Example domains |
|---|---|
| Newly blocked | NVIDIA app telemetry, Microsoft App Center, PostHog, LG TV ad platform |
| No longer blocked | doubleclick.net, www.googletagmanager.com, googleadservices.com, Reddit’s ad pixel, Adobe Analytics collection |
| Still blocked | Google Analytics collection, Marketo’s tracking script, Firebase analytics, Facebook Graph API |
| Unaffected | Google, YouTube, Lazada, Shopee, my bank, Microsoft 365 sign-in, Teams |
The “no longer blocked” row looks wrong, but it’s a deliberate choice by HaGeZi. It doesn’t block the root ad domains that break sites or tools when blocked. It blocks the specific hosts that serve the ads instead. ad.doubleclick.net resolves, while googleads.g.doubleclick.net and pubads.g.doubleclick.net were among the most-blocked domains of the week.
I work in digital marketing, so this matters to me more than to most. The old stack blocked doubleclick.net, googleadservices.com and www.googletagmanager.com outright. Those are the same root domains that Tag Manager, Google Ads previews and tag debugging depend on. HaGeZi gives me the ad blocking without breaking my work tools.
Outcomes
Blocking held up, and then some
The obvious metric is the Pi-hole dashboard’s “% blocked”. It nearly misled me.
During the trial I found that one container was making about 40,000 queries a day to Google. It was a small status panel that takes a headless Chromium screenshot every 60 seconds, and it starts with a fresh browser profile each time. Every launch did Chrome’s full first-run routine: component updater, variations service, account checks. I fixed that halfway through the trial. Total query volume dropped by about 40,000 a day, and blocked % rose for reasons unrelated to the blocklist.
So I excluded that client from both sides of the comparison:
| Day | Queries | Blocked | Blocked % |
|---|---|---|---|
| 28 Sep | 101,071 | 18,082 | 17.9% |
| 29 Sep | 90,806 | 6,117 | 6.7% |
| 30 Sep | 90,162 | 9,428 | 10.5% |
| 1 Oct | 107,609 | 15,123 | 14.1% |
| 2 Oct | 80,505 | 5,855 | 7.3% |
| 3 Oct | 95,005 | 8,570 | 9.0% |
| 4 Oct (switch) | 95,025 | 13,664 | 14.4% |
| 5 Oct | 81,460 | 9,241 | 11.3% |
| 6 Oct | 89,754 | 19,600 | 21.8% |
| 7 Oct (part day) | 50,845 | 4,156 | 8.2% |
| Period | Queries | Blocked | Blocked % |
|---|---|---|---|
| Six days before | 565,158 | 63,175 | 11.2% |
| Three days after | 222,059 | 32,997 | 14.9%, up 3.7 points |
Look at the daily range before the change: 6.7% to 17.9%. Behaviour drives this number more than the list does. The 21.8% on 6 October came mostly from Teams telemetry, an in-app ad SDK, and the NVIDIA app all retrying blocked endpoints. The honest conclusion is that blocking held up and probably improved. I wouldn’t claim more than that from three days.
What it actually blocked
These are the most-queried domains blocked during the trial that none of the fourteen old lists contained (checked against the pre-trial gravity backup):
| Domain | Blocked queries | What it is |
|---|---|---|
teams.events.data.microsoft.com | 4,227 | Teams telemetry |
b.applovin.com | 3,620 | Mobile ad SDK |
lightstep.kaizen.nvidia.com | 1,545 | NVIDIA app tracing |
http-intake.logs.us5.datadoghq.com | 1,336 | Third-party app logging |
app.posthog.com | 1,200 | Product analytics |
in.appcenter.ms | 969 | Microsoft App Center |
Two other big hitters, ms.applovin.com (4,350) and w3-reporting.reddit.com (2,708), were already blocked by the old lists. HaGeZi kept them blocked.
What broke
Nothing I noticed over three days. Every newly blocked domain in the query log was telemetry, ads or analytics. That covered Shopee and Lazada tracking endpoints, my bank’s analytics host, and Zoom’s log gateway, and none of those apps misbehaved.
One caveat. The LG TV platform domain I was most worried about never got queried during the trial, so that case remains untested.
What I got wrong
On day one I recorded my employer’s Adobe Analytics endpoint as “still blocked”. It wasn’t. When I checked again after rollout, it resolved on both Pi-holes. The only list that had ever blocked it was EasyPrivacy, one of the ones I’d disabled. HaGeZi lists neither that hostname nor the Adobe collection host it points to.
As it happens, that’s the result I’d have chosen. My employer’s analytics now fire from my home network the way they do for every other visitor. But I’d written it down wrong, and I only caught it because I checked again after rollout instead of trusting the trial notes.
The second surprise was upstream. Between trial start and rollout, HaGeZi’s own build shrank from 227,420 entries to 198,641. A maintained list changes underneath you, which is the reason to pick one. Still, it means a trial snapshot is a snapshot.
What the test exposed
Comparing the two Pi-holes’ query logs turned up a problem bigger than the blocklists. Nearly every query from the main LAN arrived from the router’s IP address.
The router was handing out itself as the DNS server and forwarding everything to the primary Pi-hole only. A masquerade rule on the LAN bridge then rewrote the source address. As a result, the Pi-hole couldn’t tell one device from another. Worse, the secondary was never used by the main LAN at all. My “redundant” DNS had a single point of failure.
Setting reverse lookups for the LAN (dns.revServers) also stopped about 18,000 PTR queries a day from the router and 13,600 from one container. Before that change, those lookups had nowhere useful to go.
Fixing the router’s DHCP DNS so that clients get both Pi-holes directly is the next job, and probably the next post.
Rollout
On 7 October I restarted the sync. One run pushed everything to the secondary. Then I checked rather than trusted:
| Check | Primary | Secondary |
|---|---|---|
| Adlists (id, enabled, count, status) | identical | identical |
| Allow/deny entries | 16, same hash | 16, same hash |
| Reverse DNS config | set | set |
| Gravity domains | 210,840 | 210,840 |
Live lookups for a test set of blocked and allowed domains matched on both. The disabled lists stay disabled rather than deleted, because that costs nothing and makes a rollback one SQL statement:
pihole-FTL sqlite3 /etc/pihole/gravity.db "UPDATE adlist SET enabled = (id<>15);" && pihole -g
What I’d tell someone
Test a blocklist on your own traffic, not its domain count. A week of query history and a few lines of SQL answered a question no comparison chart could.
Run a control. Pausing the sync gave me a second resolver on the old lists to compare against, at no extra cost.
Check your denominators. Over a quarter of my daily queries vanished halfway through for an unrelated reason. Without excluding that client, I’d have credited the blocklist with a bump it didn’t earn.
Verify after rollout, not just during the trial. The one thing I got wrong only showed up when I looked again.