Skip to main content
· Homelab · 12 min read

Technitium DNS Is Probably Eating Your Disk

If you run Technitium DNS in a container on shared spinning storage, it is almost certainly writing far more than you think. Mine was writing 25 GB a day onto a 4.9 GB disk whose used space never moved off 3.2 GB.

The culprit is the Query Logs (Sqlite) app, which is installed and enabled on a lot of setups. It logs every query you resolve into a small SQLite database, rewrites the same pages in place forever, and your storage stack multiplies that by roughly nine on the way to the platter.

One setting fixed it. Writes went to 0.88 GB a day — 42x less IOPS — and I lost nothing I was actually using.

Checking Your Own

The symptom looks like a disk filling up, and it is the exact opposite: lots of writing, no growth.

If you are on Proxmox with the PVE Prometheus exporter, you can check without even logging into the DNS box. Swap in your guest ID:

# GB written over the last week
increase(pve_disk_write_bytes{id="lxc/111", collector="node"}[7d]) / 1073741824

# GB actually used right now
pve_disk_usage_bytes{id="lxc/111", collector="node"} / 1073741824

Mine came back 151.4 and 3.20. A hundred and fifty gigabytes written, three stored, and the stored number flat all week.

No Prometheus, or not on Proxmox? Read it from inside the container. Technitium runs as a single .NET process, so attribution is easy:

P=$(pgrep -f DnsServerApp | head -1)
A=$(awk '/^write_bytes/{print $2}' /proc/$P/io)
sleep 60
B=$(awk '/^write_bytes/{print $2}' /proc/$P/io)
echo "$(( (B-A)/60 )) B/s"

Anything in the tens of KB/s, sustained, is this problem. Mine sat at 57 KB/s at three in the morning, same as it did at noon — that flatness is the tell. A backup job spikes. This just grinds.

Watching It Happen

You can see the actual mechanism in about fifteen seconds, and it is worth doing because it makes the fix obvious:

for i in $(seq 6); do
  stat -c '%y %s' '/etc/dns/apps/Query Logs (Sqlite)/querylogs.db'
  sleep 3
done
2026-08-18 14:14:55 9621504
2026-08-18 14:15:00 9621504
2026-08-18 14:15:04 9621504
2026-08-18 14:15:07 9621504
2026-08-18 14:15:09 9621504

The mtime moves every few seconds. The size does not change by a single byte.

Inserts and retention-deletes have reached equilibrium, so SQLite just recycles free pages inside a fixed 9.6 MB file, forever. Perfectly stable database. Perfectly relentless writer.

If you want to confirm the whole picture, find will show you every file Technitium has touched recently:

find /etc/dns -type f -mmin -15 -exec ls -lh {} \;
-rw-r--r-- 1 root root 3.2M Aug 18 14:12 /etc/dns/logs/2026-08-01.log
-rw-r--r-- 1 root root 205K Aug 18 14:00 /etc/dns/stats/2026080111.stat
-rw-r--r-- 1 root root 9.2M Aug 18 14:13 /etc/dns/apps/Query Logs (Sqlite)/querylogs.db

Two things that cost me time and might cost you some: /etc/dns is the data directory (/opt/technitium/dns is just binaries), and the systemd unit is technitium.service, not the dns.service you will find in older write-ups.

One last detail worth noticing:

ls '/etc/dns/apps/Query Logs (Sqlite)/' | grep querylogs
# querylogs.db          ← no -wal file

No -wal. In WAL mode SQLite keeps that file around permanently, so its absence means rollback-journal mode: write a journal, fsync, write the database pages, fsync again, delete the journal. Two files and two syncs for every batch of queries.

Why 5 GB of Writes Becomes 25

This part confused me for a while, because the two numbers I had did not agree. The process said 57 KB/s. Prometheus, watching the same container from the host side, said 490 KB/s.

LayerRatePer day
Process write_bytes57 KB/s~4.9 GB
Container blkio490 KB/s~25.7 GB
Amplification~9x

Neither was wrong. Three layers each take a cut and they compound:

SQLite’s rollback journal — two files, two fsyncs, several 4 KB pages per commit. The ext4 journal — metadata for both of those files, journaled, every time. And LVM-thin copy-on-write, which is the expensive one: thin volumes allocate in 64–256 KB chunks, and a 4 KB write into a chunk that has not been touched forces the whole chunk to be read, copied and rewritten. Scattered 4 KB updates across a 9 MB file is about the worst thing you can do to it.

%%{ init: { 'look': 'handDrawn' } }%%
graph TD
    A["One batch of queries logged<br/>4 KB of actual intent"] --> B["SQLite rollback journal<br/>2 files, 2 fsyncs per commit"]
    B --> C["ext4 journal<br/>metadata for both files"]
    C --> D["LVM-thin copy-on-write<br/>64-256 KB chunk rewritten<br/>to change 4 KB inside it"]
    D --> E["~36 KB reaches the platter<br/>~9x amplification"]

Stack those and 4 KB of intent turns into roughly 36 KB of head movement.

The useful takeaway is that this is a write shape problem, not a write volume problem. Small, random, frequently-synced writes are what shared thin-provisioned storage punishes. An append-only file ten times bigger would cost you less.

The Two Settings That Look Right and Do Nothing

The config lives next to the database. Every value here is stock:

{
  "enableLogging": true,
  "maxQueueSize": 200000,
  "maxLogDays": 7,
  "maxLogRecords": 10000,
  "enableVacuum": false,
  "useInMemoryDb": false,
  "sqliteDbPath": "querylogs.db",
  "connectionString": "Data Source='{sqliteDbPath}'; Cache=Shared;"
}

My instinct was to cut maxLogDays from 7 to 1. It is the retention knob, the file is a log, the file is too busy — obviously turn it down.

It would have done nothing. Retention controls how much you keep, not how much you write, and the writes here are inserts. A shorter window gives you a smaller database that gets hammered exactly as hard.

enableVacuum is worse than useless — it adds periodic full-file rewrites on top of everything else.

That trap is not specific to Technitium. If your problem is throughput rather than capacity, retention settings are the wrong dial and will happily eat an afternoon proving it.

The Setting That Works

It is two lines above the ones that don’t:

# /etc/dns/apps/Query Logs (Sqlite)/dnsApp.config
-  "useInMemoryDb": false,
+  "useInMemoryDb": true,

This moves the query log into an in-memory SQLite database. The query log UI still works — you can still search recent queries when some device starts behaving strangely, which is the only time I have ever opened it. What you give up is that history surviving a restart.

For a home resolver that trade has an obvious winner. What I need is name resolution, continuously. A query log that survives reboots is worth a lot less than a disk that does.

Check your memory headroom before flipping it, though. The database was 9.6 MB on disk and the container has 2 GB with 1.4 GB free, so this was not a close call — but a busy recursive resolver with long retention is a different sum:

free -m
du -h '/etc/dns/apps/Query Logs (Sqlite)/querylogs.db'

Back up both files first:

D='/etc/dns/apps/Query Logs (Sqlite)'
cp -a "$D/dnsApp.config" "$D/dnsApp.config.bak"
cp -a "$D/querylogs.db" /root/querylogs.db.bak

sed -i 's/"useInMemoryDb": false/"useInMemoryDb": true/' "$D/dnsApp.config"

The restart takes DNS down for your whole network. If this is your only resolver, every device loses name resolution while it comes back — mine was down about eight seconds. Pick a quiet moment, and make sure you can still reach the box by IP if it doesn’t return.

The config is only read when the app loads, so editing it alone changes nothing. The restart is the point:

systemctl restart technitium.service

If it goes badly:

cp -a "$D/dnsApp.config.bak" "$D/dnsApp.config"
systemctl restart technitium.service

Checking It Worked

Test resolution from somewhere else — querying the resolver from the resolver proves nothing:

dig +short @10.10.20.53 github.com

Then confirm the writer actually stopped:

stat -c '%y %s' '/etc/dns/apps/Query Logs (Sqlite)/querylogs.db'
# frozen at the restart, and it never moves again

One thing that nearly fooled me here: restarting Technitium re-downloads every blocklist you have configured, about 50 MB in my case. It pushed my five-minute write rate to 1.45 MB/s straight after the change and made the fix look like a failure. Give it ten minutes before you believe any number.

Once it settled, on the same query load:

MetricBeforeAfter
Container write rate0.42 MB/s0.010 MB/s42x
Container writes per day25.7 GB0.88 GB29x
Process write_bytes57 KB/s1.4 KB/s41x
querylogs.db mtimeevery 2–5 sfrozenstopped
Disk usage3.204 GB3.215 GBstill flat
Host node CPU18.99%16.62%−2.4 pp

It went from the single loudest disk writer among 46 guests to an unremarkable one. Residual amplification dropped to about 7.5x, because what is left is an append-only log and an hourly stats file — same storage stack, different write shape, wildly different cost.

While You’re In There

A few smaller things Technitium leaves lying around, none of which are IOPS problems but all of which are worth an hour.

Nothing prunes the service logs. Not the query log — the operational one in /etc/dns/logs/. Mine had 198 files and 1.7 GB going back six months, single days as large as 57 MB, which was about half the container’s used disk:

du -sh /etc/dns/logs && ls /etc/dns/logs | wc -l

There is a retention setting in the admin UI under Settings → Logging, or you can just do it yourself:

find /etc/dns/logs -name '*.log' -mtime +30 -delete

Read what’s in them before you delete them. Mine were running 11,000 lines a day, and a good chunk was one internal hostname failing to resolve upward, each failure with a full .NET stack trace attached:

DnsServerCore.Dns.DnsServerException: All name servers failed to answer the request
'name.internal.example. A IN'. Received last response with RCODE=Refused.

That is a missing internal record being forwarded out to public resolvers. Costs nothing in throughput, but it is noise in the file you will want to read next time something actually breaks.

And if you have any SSD on the host at all, bind-mounting /etc/dns onto it beats every individual setting in this post. It removes the amplification at the source instead of removing one thing that triggers it.

If You Actually Need the Query History

In-memory is right for most self-hosted setups, but if you genuinely need query logs across restarts, in rough order of effort:

  1. Move the data directory to an SSD. Fixes the root cause rather than a symptom.
  2. Switch SQLite to WAL mode — turns the two-fsync commit into an append, which is dramatically cheaper here. Technitium doesn’t expose journal_mode in the app config, so this means patching the connection string.
  3. Bigger LVM-thin chunk size, or a thick LV. Attacks the third layer of the amplification. Invasive, since it means rebuilding the volume, but it helps every guest on that pool.
  4. Uninstall the Query Logs app. Zero writes, zero memory, no per-query visibility at all. Fine if you have never opened it.

One Thing That Nearly Sent Me the Wrong Way

While reading the graphs I found something that looked like a smoking gun. On a Friday afternoon the write rate halved on its own — 0.34 MB/s down to 0.10 — and stayed there. No deploy, no config change, nothing in any log.

An unexplained improvement is more worrying than an unexplained regression. If something invisible had already fixed half the problem, my fix would be sitting on a baseline that could revert whenever it felt like it, and nothing in the data would tell me which was which.

Pulling nine days instead of three dissolved it:

WindowRate
Mon–Fri midday0.16 – 0.63 MB/s
Fri midday – Sun0.09 – 0.21 MB/s
Monday onward0.16 – 0.48 MB/s

Those quiet days were Saturday and Sunday. Fewer devices awake, fewer queries, fewer commits, and it came back up on Monday all by itself. No mystery actor — just a household, and a metric honestly reporting how many people were home.

Worth remembering generally: before you treat a step change as a change, widen the window far enough to see whether it is a cycle.

The Short Version

  • Compare a week of writes against current disk usage. Flat usage with heavy writes means rewrites, not growth.
  • Watch stat -c '%y %s' on querylogs.db in a loop. Moving mtime, static size, and that’s your answer.
  • Ignore maxLogDays and enableVacuum. Wrong dial entirely.
  • Back up, set "useInMemoryDb": true, restart technitium.service, accept a few seconds of network-wide downtime.
  • Verify from a different host, and ignore the first ten minutes while blocklists re-download.
  • Prune /etc/dns/logs/ while you’re there.
  • If you own an SSD, put /etc/dns on it and skip most of this.

None of this is Technitium being badly written. Its defaults were chosen for a server that owns its disk, and they get expensive the moment you put it on a thin-provisioned container over shared spinning storage — which is exactly where most self-hosted DNS actually lives.

The same logic applies to everything else on that host with an embedded SQLite database and a “log every event” option that ships enabled: metrics agents, ad blockers, media indexers. Mine had been quietly writing 150 GB a week for the sake of a query log I had never once opened.