A 2.2 Million Rule Blocklist Kept OOM-Killing My Backup AdGuard DNS
One HaGeZi threat list pinned my backup AdGuard Home at its 750 MB memory cap and got it OOM-killed three nights running. Why the logs blamed the upstreams, and how 91M queries picked a list.
HomeLab, Network and DNS. Updated . 5 min read.
My backup DNS server was pingable, its tunnel was fine, and it could reach Cloudflare over DoH in 45 ms. It still timed out on almost every query. AdGuard's own log blamed every upstream it had. The upstreams were fine. One blocklist had eaten all its memory.
TL;DR: the secondary AdGuard Home runs on a small VPS with a 750 MB container limit. The HaGeZi Threat Intelligence Feeds (TIF) full list, 2,221,134 rules and 45 MB of text, kept it pinned at 749.8 MiB. The kernel thrashed its code pages off disk and OOM-killed it on Sep 21, 22 and 23. I swapped the full list for TIF Medium after checking it against 91 million past queries: the full list's unique catch was 4 domains in 8 months, and Medium keeps 3. The secondary dropped to 305 MiB and the timeouts stopped.
Why the backup DNS matters more than it looks
My setup has two AdGuard Home servers. The primary runs on the NUC. The secondary runs on a VPS that the router reaches over WireGuard. AdGuardHomeSync copies lists, user rules, and DNS settings from the primary to the secondary every 10 minutes, so I only edit one place.
The router's DHCP hands out the NUC first and the VPS second. You'd expect the second server to sit idle until the first dies. It doesn't. Musl-based containers ask both servers at once, and Windows moves to the second one after a second without an answer. In a September sample the secondary answered 5.6% of the NUC's queries and 15.9% of my PC's. A broken secondary isn't a dormant problem. It's random slow page loads.
What the box looked like
The network was the first suspect, and it was clean:
- ping to the VPS over the tunnel: 120 ms, normal for that link
- a direct DoH request from the VPS to Cloudflare: 45 ms
digagainst the secondary, from my PC and on the VPS itself: timeout
Then memory. The VPS has 967 MB of RAM and had 27 MB available. The compose file caps AdGuard at mem_limit: 750m with no swap, and AdGuard was using 749.8 MiB. The cgroup files told the rest:
memory.pressure full avg60=31%
memory.events max 848077
full avg60=31% means every task in the container was stalled on memory about a third of the time. max 848077 is how many times the container ran into its limit. And vmstat showed 280 MB/s of disk reads on a box whose only job is DNS.
The kernel log had the end of each episode: AdGuard OOM-killed on Sep 21, Sep 22 and Sep 23. That log goes back to Mar 1 and has no kill before Sep 21.
Why it timed out instead of just dying
This is the part that makes the logs misleading. A container at its memory limit doesn't get killed right away. The kernel reclaims first, and the cheapest thing to reclaim is page cache. That includes the pages of the AdGuard binary itself. So the kernel throws out AdGuard's code, AdGuard runs the next instruction, and the kernel reads it back from disk. That's the 280 MB/s.
A process in that state is alive, just very slow. Slow enough that its own outgoing DoH requests hit their deadline, so the AdGuard log filled with Client.Timeout exceeded for every upstream, all four providers. If you read only that log, you go looking at the upstreams or the tunnel. Neither was the problem. The OOM kill was the kernel finally giving up, and each restart put AdGuard straight back on the same path.
One list was the memory
AdGuard keeps every loaded rule in memory. The TIF full list alone was 45 MB with 2,221,134 rules. All my other lists together were about 6 MB.
The sync made this a two-box problem. The primary had the same list set, and it was using 735 MiB. It just doesn't live in a 750 MB box. The secondary did, with the lists reloading every hour on top (filters_update_interval: 1). It also explains why removing the list only on the VPS would not work. AdGuardHomeSync copies the list set as one unit, so the next sync run puts the list back. The fix had to happen on the primary.
There were three options:
| Option | Cost |
|---|---|
| Swap TIF full for a smaller HaGeZi TIF variant, on the primary | less threat coverage, amount unknown |
Bigger VPS, raise mem_limit | roughly $7 more a month, by my estimate |
| Keep TIF full on the NUC only, stop syncing lists | a second list set to maintain by hand |
Asking my own traffic what the list was worth
"Less threat coverage" was the part I didn't want to guess at. So I checked the question against history: AdGuard's stats and query logs from both servers, January 16 to September 23, about 91 million queries. For each list I counted the unique blocks, meaning queries that list blocked and no other list I run would have blocked. That's exactly what you lose when you remove a list.
| List | Unique blocks, about 8 months |
|---|---|
| HaGeZi Pro | 11.5M queries, 45,000 domains |
| Windows/Office tracker | 543,000 |
| Encrypted DNS bypass | 139,000 |
| Adobe | 13,800 |
| DynDNS | 4,600 |
| Smart-TV | 1,400 |
| Badware Hoster | 188 |
| TIF full | 87 queries, 4 domains |
| Xiaomi, Samsung | 0 |
The live stats agreed. At the time of blocking, AdGuard had credited TIF with 1 block in 2.6 million queries, and 0 of 791,000 on the primary in the last 31 hours. TIF is a threat list for malware, phishing, and scam domains. It doesn't block ads. HaGeZi Pro does that work, and Pro didn't change.
Then the smaller variants against those 4 domains. TIF Medium, with 931,000 rules, still blocks 3 of them. The one it misses showed up twice in 8 months. So the measured cost of the swap was 1 domain and 2 queries.
There's a real caveat. Threat lists drop old domains quickly, so a history check undercounts what a list used to catch. And the full list's job is the threat you haven't visited yet. History can't measure that. What it can say is that the extra 1.3 million entries almost never matched anything I did, and that the full list had just taken DNS down three times. A resolver that times out protects nobody.
Out of curiosity I ran the other security lists the same way: TIF Mini, Phishing Army, PhishTank, URLHaus, a scam list, uBO Badware. Zero extra hits in 8 months. Adding them would buy nothing.
The change
All on the primary, then one sync run:
- Removed the TIF full list and added TIF Medium by HaGeZi's raw GitHub URL, not the registry entry, which is the full list.
- Changed the list update interval from 1 hour to 12 hours.
- Upgraded the secondary from v0.107.72 to v0.107.79 to match the primary, so the sync stopped logging
Versions do not match. - Tidied the user rules and upstreams in the same pass. Sentry and the Google Ads and Analytics consoles are now allowed for my PC only, a few torrent trackers are unblocked, and six telemetry hosts the lists miss are blocked. Cloudflare is now reached by IP, and the upstream timeout is 3 seconds with fallback servers.
I ran the sync by hand instead of waiting for the 10-minute run. It took 14 seconds.
How I know it held
On the secondary, right after the change:
- AdGuard from 550 MiB just before the sync to 305 MiB, against the 750 MiB limit
- host available memory from 152 MB just before the sync to 344 MB
- memory pressure near zero
- 15 of 15 uncached lookups answered in 120 to 132 ms, which is the tunnel round trip and nothing more
A later checkup the same evening showed it settled around 440 MiB, with no restarts, no OOM kills, and memory.events max still at 0. The primary dropped too, from 735 to 411 MiB. Most of that 324 MiB is likely the 1.3 million rules that separate the two TIF variants.
The note I left for next time: if the secondary climbs toward its cap again, stop syncing filter lists and give it a lighter set as a pure fallback. For now it doesn't need that, and one list set on both servers means the second DNS server blocks the same things as the first.
Questions
- Why does AdGuard Home time out instead of crashing when it runs out of memory?
- A container at its memory limit gets reclaimed before it gets killed. The kernel drops page cache, including the code pages of the AdGuard binary, and reads them back from disk on the next use. AdGuard stalls, its own upstream requests hit Client.Timeout, and clients see timeouts. The OOM kill comes later, if at all.
- How much memory does the HaGeZi TIF full list take in AdGuard Home?
- On my primary AdGuard, swapping TIF full (2,221,134 rules, 45 MB) for TIF Medium (931k rules) took it from 735 to 411 MiB. With the full list, the secondary sat at 749.8 of a 750 MiB container limit.
- Is HaGeZi TIF Medium enough, or do I need the full TIF list?
- In 91 million of my own queries over about 8 months, TIF full blocked 4 domains that no other list blocked, 87 queries in total. Medium still blocks 3 of them. History can't show new threats that only the full list has, so it's a trade-off, but a DNS server that times out protects nothing.
- Why change the blocklist on the primary when the secondary AdGuard is the one failing?
- AdGuardHomeSync copies the filter lists from the primary to the secondary as one set. Remove a list on the secondary alone and the next sync puts it back. The change has to happen on the primary.