When I wiped my Home Assistant box on purpose last week, the part that surprised me wasn’t the restore. That went fine. The part that surprised me was watching the progress bar sit there while a single file unpacked, and realising that one file — home-assistant_v2.db — was 5.8 GB.
My entire configuration directory, minus the database, is under 400 MB. Everything I have ever written, every automation, every dashboard, every ESPHome YAML file, every secret: 400 MB. The database was fourteen times that, and I had never once looked at it.
So I looked. This is what I found and what I changed. I’m running Home Assistant OS 2026.8.3 bare-metal on a used HP EliteDesk 800 G4 mini PC with a SATA SSD, which means none of this is an SD-card-wear panic. It’s just housekeeping I’d been putting off for two years.
First, what the database is actually holding
The Recorder integration writes a row every single time an entity changes state. Not every minute — every change. That’s the bit that gets people. A temperature sensor that reports every five minutes is nothing. A Zigbee link quality sensor that reports every time a packet lands is a firehose.
By default Recorder keeps 10 days of that history (purge_keep_days), purges nightly at 04:12 local time, and repacks the database every second Sunday after the purge. All of that is on out of the box, and all of it was working correctly on my system. My database wasn’t 5.8 GB because purging was broken. It was 5.8 GB because ten days of my entity churn is genuinely that big.
Worth knowing before you start cutting: long-term statistics are a separate thing. Those hourly and five-minute rollups that feed the Energy dashboard and your year-over-year graphs are not deleted by purge_keep_days. You can be aggressive with state history and still keep the long arc of your energy data. That distinction is the single most useful thing to understand here, and it’s the reason I stopped being precious about raw states.
Measuring before cutting
I didn’t want to guess. The SQL integration can point at the Recorder database directly, and the docs give you the query for size:
SELECT ROUND(page_count * page_size / 1024 / 1024, 1) as size
FROM pragma_page_count(), pragma_page_size();
Add that through the UI, use size as the value column, set the unit to MiB and the device class to Data size. If you’re on Recorder’s default SQLite you don’t even need to supply a database URL — it finds it. Now you have a sensor, which means you have a graph, which means you can actually see whether a change helped instead of vibing it.
To find the offenders themselves I sat with the History panel and the entity list for twenty minutes, sorted by “last changed”, and wrote down anything that had updated in the last sixty seconds and that I could not imagine ever wanting to look at historically. That list was depressingly easy to build.

What I excluded
Recorder filters live in configuration.yaml, not the UI. Here’s roughly what mine looks like now, trimmed of the site-specific bits:
recorder:
purge_keep_days: 7
commit_interval: 30
exclude:
domains:
- automation
- update
- device_tracker
entity_globs:
- sensor.*_linkquality
- sensor.*_rssi
- sensor.*_signal_strength
- sensor.*_last_seen
- sensor.*_uptime
- sensor.sun*
entities:
- sensor.date
- sensor.time
- sun.sun
The entity_globs block is doing almost all of the work. Every Zigbee device I own publishes a link quality value and a “last seen” timestamp through Zigbee2MQTT, and with a few dozen devices on the mesh those two patterns alone were the largest single category of rows in the database. I care about link quality when I’m debugging a mesh, and when I’m debugging a mesh I want live values, not last Tuesday’s. The update domain was another one — an entity per add-on and integration, all of them writing rows, none of them interesting after the fact.
Two syntax notes from the docs that saved me a restart: globs support * for zero-or-more characters and ? for zero-or-one, and if you mix include and exclude the precedence rules are specific enough that it’s worth reading them rather than assuming. I stuck to a pure exclude list precisely so I wouldn’t have to think about precedence. Allowlists are tidier in theory; in practice an allowlist means every new device you add is silently not recorded until you remember to update the file, and I know myself.
I also raised commit_interval from the default 5 seconds to 30. The docs suggest this mainly for SD-card installs, but there’s no reason a mini PC needs to fsync twenty times a minute either, and the history and activity panels don’t lag as a result — they get changes streamed to them immediately regardless of when the write hits disk.
The repack nobody warns you about
Excluding entities stops new rows. It does not remove old ones, and it does not shrink the file. SQLite hangs on to the pages. To actually reclaim space you call recorder.purge from Developer Tools with repack enabled, and then you wait.
Here’s the part I’d underestimated: a repack needs room. The Recorder docs are blunt about it — you need at least as much free disk space as the database occupies, at all times, because a rebuild or repack can copy the data on disk. And in the disk-corruption case, where SQLite moves the broken database aside and starts a fresh one to keep you online, they want 2.5x free. On a 5.8 GB database on a small SSD, that stops being a rounding error. Check your free space before you press the button, not after.
If you want to nuke history for specific entities without waiting for the retention window, recorder.purge_entities takes entity IDs, domains or glob patterns. I used it once on the link quality sensors and it did in a minute what dropping purge_keep_days would have taken a week to do.
What I chose not to do
I did not move to MariaDB. It’s supported — MariaDB 10.3+, MySQL 8.0+, PostgreSQL 12+ all work — and if you’re running a big installation with Grafana on the side there’s a real argument for it. But the Recorder documentation says plainly that SQLite is the most tested option and that recent Home Assistant versions are heavily optimised for it, and it warns that choosing something else makes you the database administrator, including for backups of that external database. My whole reason for running bare-metal HA OS is that there’s exactly one thing to back up. Adding a second database engine to save a few hundred milliseconds on history queries I run twice a month is a bad trade for me. It might not be for you.
I also didn’t go below seven days of retention. I was tempted by five. But roughly once a quarter something misbehaves on a weekend and I don’t get to it until the following weekend, and a history window shorter than that turns “let me look at what happened” into “let me guess at what happened”. Seven days is the number where I stop losing the ability to debug my own house.
Was it worth it?
After the excludes, a purge and a repack, the database settled at just under 700 MB and has stayed roughly flat for a week. That’s an eight-fold reduction, and the honest answer to “did anything get faster?” is: the dashboard history graphs load noticeably quicker, and I have lost nothing I actually miss.
The bigger win is less visible. My nightly backup is now small enough that uploading it offsite is a non-event, and the next time I do something reckless to this machine, the restore won’t be dominated by a file full of signal strength readings from a fortnight ago. That’s the whole point. The database exists to help me understand my house, not to archive the noise the house makes while it’s working.
If you’ve never checked, go check. Settings → System → Storage will tell you the shape of it in about ten seconds, and the SQL sensor above will keep telling you afterwards. Mine had been quietly growing since 2024 and I only noticed because I broke my own system on purpose.

Leave a Reply