I cut my Home Assistant backups in half

Disclosure: I work for Apollo Automation. The radar devices I mention below are ours.

My Home Assistant backups had quietly gotten huge. When I went looking for why, it was the recorder database. It’s the default SQLite database, I keep 30 days of history, and it had grown to over 7GB. Your database goes into every full backup, so a fat database means fat backups, and mine were dragging.

I’d already deleted a few obvious things before I really dug in, which knocked it down a bit, but it was still way bigger than it had any right to be. The thing that finally clicked is that I’d misunderstood what excludes even do.

Excludes only stop new data

Here’s the part that tripped me up. An exclude in your recorder config stops new rows from being written for those entities. It does nothing to the history already sitting in the database, and SQLite won’t shrink the file on its own even after rows get deleted.

So I could exclude a chatty sensor today and still be hauling around months of its junk tomorrow. The file just sat there. If you’ve been adding excludes and wondering why your database (and your backups) keep growing, that’s why.

To actually get the space back you need two more steps: delete the old rows, then rewrite the file so the disk space comes back.

What I actually ran

The order matters, mostly so you only rewrite the giant file once.

  1. Add the excludes to your config first. This stops everything from refilling the second you clean up. I skipped this on one entity and watched it come right back.
  2. Delete the old rows with recorder.purge_entities. Use keep_days: 0 and entity_globs so it wipes everything for those entities no matter your retention window.
  3. Reclaim the space with recorder.purge and repack: true. The repack is what rewrites the file and actually shrinks it on disk.

You don’t need to write any scripts for steps 2 and 3. Both are built-in actions you can run right from the GUI. Go to Developer Tools, open the Actions tab, search for recorder.purge_entities or recorder.purge, fill in the fields (or flip to YAML mode for the entity globs), and hit the button. The only part that needs a real config edit is the excludes in step 1.

A couple things that wasted my time so they don’t waste yours:

Running recorder.purge with keep_days: 30 deletes almost nothing, because all your data is already inside the 30-day window you’re keeping. That’s the whole reason step 2 uses purge_entities with keep_days: 0.

And the database size won’t drop until the repack finishes. It updates on a delay and looks frozen halfway through. That’s normal. Repacking a multi-GB file took me 10 to 20 minutes. Just let it run.

What was eating all the space

I never would have found these by guessing. The thing that made it easy was the DbStats addon, which ranks every entity by how many rows and how many bytes it’s using. Once I had that list sorted, the offenders were obvious, and none of it was data I’d ever miss:

  • sensor.*_last_seen heartbeats. Every device with a “last seen” timestamp was writing around 156 rows an hour, each, across dozens of devices. Pure noise.
  • Idle power sensors flickering. A light switch doing nothing was bouncing between 1.8W and 1.9W and logging about 129 rows an hour. The better fix here is at the device instead of excluding the whole sensor: bump the power-report threshold (Z-Wave JS reporting thresholds, or an ESPHome delta/throttle filter) so it only reports real changes.
  • mmwave radar distance sensors. This was the big one I found on a second pass. The *_radar_detection_distance sensors were logging roughly 482 rows an hour each, with the moving-distance and per-target ones close behind, across all my Apollo radar devices. I’d excluded the still-distance and per-gate energy sensors earlier and completely missed these.

The common thread: high-frequency sensors that change constantly but whose history you never actually look at.

Where it landed

First pass on the heartbeats and the obvious junk plus a repack got the database from over 7GB down to about 3.6GB. The second pass once I caught the radar sensors took it lower still. Same hardware, same 30 days of history, less than half the size, and my backups shrank right along with it.

Stuff I’d tell myself before starting

  • Install the DbStats addon before you do anything else. Guessing at offenders is slow. Its ranked per-entity list of rows and bytes turns the whole hunt into a five-minute job, and it’s how I found every offender above.
  • Fix the noise at the source when you can. Excluding a power sensor outright loses the real readings too. Raising the report threshold keeps the good data and kills the flicker.
  • If your instance is genuinely busy (Frigate, lots of Z-Wave power, a wall of motion and radar) and SQLite stays heavy even after all this, moving the recorder to MariaDB is the bigger next step. I’m not there yet.
  • Dropping retention from 30 days to 10 is the other big lever, and I’m keeping it in my back pocket. I’d rather keep the history and cut the noise.

If your backups are fat and your excludes don’t seem to do anything, now you know why. Stop the new data, delete the old, repack to get the space back. That’s the whole thing.

P.S. it’s my birthday today, I turned 38. I spent part of it shrinking a database, which tells you everything you need to know about me.

← All posts