What a drill should check
A restore that finishes is not the same as a backup that is good. The engine starting proves the archive was readable and the version was compatible; it proves nothing about whether the data inside is the data you meant to protect. Checks are where a drill stops being a restore test and starts being evidence.
The checks that ship
Section titled “The checks that ship”service_healthy— Engine answers the adapter's healthchecktable_exists— Table exists and is queryablerow_count— Row count within boundsfreshness— Newest row younger than a maximum agesql— Custom SQL assertion (your own statement)
Choosing them
Section titled “Choosing them”Read the list as a ladder. Each rung costs more to configure than the one below it and catches a failure the one below it cannot see.
Start the engine. The healthcheck is the floor, and it belongs in every drill: without it a restore that produced an unstartable database would still be recorded as a pass. It is also the cheapest possible check, so there is no argument for leaving it out. What it cannot tell you is whether the database it started is yours.
Prove the schema arrived. A table check catches a restore that succeeded against the wrong source kind, or a dump taken with a filter nobody remembered — the archive restores, the engine starts, and the table your application needs is not there.
Prove the data arrived. A row count with a lower bound is the check that catches the failure the project exists for: months of dutifully backing up the wrong database. An empty or nearly empty table restores perfectly.
Give it a floor, not a ceiling, unless you know the table is bounded. A range tight enough to be interesting is a range that will fail on a normal Tuesday, and a check that cries wolf is a check somebody will remove.
Prove the data is current. This is the one to reach for next, and the one most often left out. A freshness check compares the newest timestamp in a table against a maximum age, which catches the failure no other check can see: the backup job that has been running perfectly against a replica that stopped replicating, or a dump path that quietly started pointing at last quarter’s file. Everything else on the ladder passes. The data is real, complete, and months old.
If a table has a timestamp column, this check is nearly free and worth more than any other single addition.
Prove what only you know. A custom statement is for invariants no generic check can express — that a ledger balances, that a foreign key has no orphans, that yesterday’s partition exists. It runs through the adapter’s own SQL runner, so it is written in the engine’s dialect, and its name is recorded in the evidence alongside its result.
A shape that works
Section titled “A shape that works”For a first drill on an unfamiliar database: the healthcheck, plus a row count with a lower bound on the table whose loss would hurt most. Two checks, five minutes of thought, and the drill already proves more than a backup log ever did.
Add freshness as soon as you can name a timestamp column. Add custom statements when an incident teaches you what you wish the drill had caught.
What checks are not for
Section titled “What checks are not for”Checks are assertions about a restored database, not a monitoring system for production. Anything that would be better as a Prometheus alert against the live database belongs there instead: a drill runs on a schedule measured in days, against data that is by definition not current, in a sandbox that is destroyed immediately afterwards.
The question a check should answer is the recovery question — if I had to use this backup, would what came back be usable? — and nothing wider.