DEV Community

Cover image for Your Database Backup Exists. But Can You Actually Restore It?
Pawan Bisht
Pawan Bisht

Posted on

Your Database Backup Exists. But Can You Actually Restore It?

Your Database Backup Exists. But Can You Actually Restore It?

Most teams have database backups.

They run on a schedule. They complete successfully. They are stored in S3, RDS, or another backup system. Monitoring tells you that the backup job is green.

Then one day, something goes wrong.

A database is corrupted.
A production deployment deletes important data.
An infrastructure failure takes down the database.
Someone needs to recover yesterday's state.

And suddenly, the important question isn't:

"Did the backup complete?"

It's:

"Can we actually restore it?"

That distinction is easy to overlook.

A successful backup job proves that data was copied somewhere. It does not necessarily prove that the backup is usable.

The backup paradox

Imagine a team with this setup:

  • PostgreSQL runs in production.
  • Automated backups run every night.
  • AWS RDS manages the snapshots.
  • The team receives an alert if a backup fails.
  • Everything has been green for the last six months.

From an operational dashboard, things look great.

But what happens if the team discovers that:

  • a required table wasn't included,
  • permissions are incorrect after restoration,
  • an application expects an index that isn't present,
  • foreign-key relationships are broken,
  • critical data is missing,
  • or restoring the snapshot takes significantly longer than the team's RTO?

The backup technically exists.

But the recovery process is broken.

This is why backup testing and restore testing are two different things.

A backup is not a recovery plan

A useful mental model is:

Backup

"We have a copy of the data."

Restore

"We can turn that copy back into a working database."

Recovery validation

"The restored database contains the data and structure our application actually needs."

That third step is where many systems stop short.

You don't necessarily need to run the entire application to validate a database restore.

You can test the database itself.

For example:

checks:
  - type: connect

  - type: schema
    expect_tables: [customers, orders]

  - type: row_count
    table: orders
    min: 1

  - type: foreign_key
    table: orders
    references: customers

  - type: golden_query
    query: "SELECT count(*) FROM orders"
    expect_min: 1
Enter fullscreen mode Exit fullscreen mode

These checks answer practical questions:

  • Can we connect?
  • Are the important tables there?
  • Is the expected data present?
  • Are relationships intact?
  • Does a critical query still work?

The exact checks will depend on the application, but the principle is the same:

Don't just verify that a backup exists. Verify that the restored state satisfies known invariants.

Why manual restore testing doesn't happen often

At this point, someone usually says:

"We already know we should test restores."

True.

The problem is that actually doing it manually is inconvenient.

A realistic restore test might require someone to:

  1. Find the correct snapshot.
  2. Restore it into a temporary database.
  3. Wait for the database to become available.
  4. Configure credentials.
  5. Connect to it.
  6. Run validation queries.
  7. Check the results.
  8. Record how long recovery took.
  9. Delete the temporary database.
  10. Repeat the process periodically.

Nobody wants to spend their Friday afternoon doing this.

So the restore test gets postponed.

Then postponed again.

Eventually the team has a backup strategy that has never been seriously exercised.

Recovery time is part of the test

There is another problem with only checking whether a restore succeeds.

How long did it take?

Suppose your recovery objective is one hour.

Your backup restores successfully, but it takes three hours.

Technically, the backup works.

Operationally, it fails your recovery requirement.

That's why restore validation should measure recovery time as well.

A useful report might look conceptually like this:

Restore Validation: PASS

Snapshot: latest
Restore time: 18m 42s

Checks:
✓ customers table exists
✓ orders table exists
✓ orders row count >= 1
✓ foreign key orders -> customers intact
✓ golden query passed

Recovery objective: 60m
Result: PASS
Enter fullscreen mode Exit fullscreen mode

Now you have evidence rather than an assumption.

What should you test?

You don't need to test every possible database property.

Start with the things that matter to your application.

1. Connectivity

Can you connect to the restored database?

This sounds obvious, but it establishes the basic recovery path.

2. Schema

Are critical tables present?

For example:

users
orders
payments
subscriptions
Enter fullscreen mode Exit fullscreen mode

A restore that is missing an important table shouldn't be considered healthy.

3. Data volume

Row counts can provide a simple sanity check.

For example:

- type: row_count
  table: orders
  min: 100
Enter fullscreen mode Exit fullscreen mode

This isn't a replacement for deep data validation, but it can detect obvious failures.

4. Relationships

Database relationships are important.

If orders.customer_id references customers.id, you want to know that the restored data doesn't contain orphaned records.

5. Critical queries

Every application has queries that represent important business functionality.

For example:

SELECT count(*) FROM orders;
Enter fullscreen mode Exit fullscreen mode

Or something more meaningful:

SELECT count(*)
FROM payments
WHERE status = 'completed';
Enter fullscreen mode Exit fullscreen mode

These can become "golden queries" that must continue to work after recovery.

6. Data freshness

For systems that depend on continuously updated data, freshness can matter as much as row count.

For example:

- type: freshness
  table: orders
  column: created_at
  max_age: 24h
Enter fullscreen mode Exit fullscreen mode

A database containing millions of rows isn't necessarily healthy if the newest data is several days old.

7. Indexes and other structural requirements

Sometimes the data is present but the database isn't operationally equivalent to what the application expects.

Critical indexes can also be validated as part of recovery testing.

The important part: automate the boring work

This is the idea behind Revenant.

Revenant is a small CLI designed around one question:

Can this database backup actually be restored and validated?

It connects to PostgreSQL and runs checks defined in a revenant.yaml file.

For a local database, the workflow can be as simple as:

export DATABASE_URL='postgres://user:pass@localhost:5432/mydb?sslmode=disable'

revenant init --plan my-app --force
revenant verify
Enter fullscreen mode Exit fullscreen mode

init can inspect an existing PostgreSQL database and generate a starting configuration.

verify then runs the defined validation checks.

For AWS RDS, the workflow can go further.

Revenant can:

  1. Find the latest RDS snapshot.
  2. Restore it into a temporary sandbox.
  3. Run the configured checks.
  4. Measure recovery time.
  5. Generate a report.
  6. Delete the sandbox.

The goal isn't to replace your backup system.

AWS RDS, PostgreSQL, cloud providers, and backup tools already do the actual backup work.

The missing layer is often proof that recovery works.

Treat recovery testing like CI

One of the most useful ways to think about restore testing is to treat it like a test suite.

You already test application code.

You might run:

go test ./...
Enter fullscreen mode Exit fullscreen mode

before merging code.

You probably test deployments.

You may run infrastructure checks.

But your backup system can also have tests:

Backup
   ↓
Restore
   ↓
Connect
   ↓
Validate schema
   ↓
Validate data
   ↓
Validate relationships
   ↓
Measure recovery time
   ↓
Generate evidence
Enter fullscreen mode Exit fullscreen mode

And this process can run periodically in CI.

For example, a weekly GitHub Actions workflow can restore a recent snapshot, execute the validation suite, and upload the resulting report.

Now recovery isn't something you hope works during an incident.

It's something you've exercised recently.

Keep the application out of the restore test

One design choice is particularly useful here.

Database recovery validation doesn't necessarily need to understand your application code.

Your application could be written in:

  • Node.js
  • Python
  • Go
  • Ruby
  • Java
  • Rails
  • Django
  • or something else.

The database doesn't care.

A database-focused recovery tool can validate the database directly through PostgreSQL.

This makes the approach language-agnostic.

You define what matters in configuration:

checks:
  - type: connect

  - type: schema
    expect_tables: [customers, orders]

  - type: row_count
    table: orders
    min: 1

  - type: foreign_key
    table: orders
    references: customers
Enter fullscreen mode Exit fullscreen mode

The tool doesn't need to inspect your models or application source code.

That separation makes the recovery test easier to run independently from the application.

Don't forget cleanup

Automated restore testing introduces another problem: temporary infrastructure.

If every restore creates an RDS instance and something goes wrong before cleanup, you can end up with abandoned resources.

That's particularly painful with cloud infrastructure because forgotten resources can become forgotten bills.

A recovery-testing system therefore needs cleanup as part of the workflow, not as an afterthought.

For example, a safety-net command can look for Revenant-managed sandbox instances older than a configured age and remove them:

revenant reap --max-age 4h --region us-east-1
Enter fullscreen mode Exit fullscreen mode

And CI workflows can run cleanup even when the validation job fails.

The rule should be simple:

If the restore test creates infrastructure, the restore test owns the cleanup.

Evidence matters

Another advantage of automated restore testing is that every run can produce evidence.

For example:

report.json
report.md
Enter fullscreen mode Exit fullscreen mode

The JSON report can be consumed by automation.

The Markdown report can be attached to a CI run, reviewed by engineers, or retained as operational evidence.

Instead of saying:

"We think our backups work."

You can say:

"The latest snapshot was restored on September 14. Recovery took 19 minutes, all six validation checks passed, and the sandbox was successfully destroyed."

That's a much stronger statement.

Start small

You don't need a huge disaster-recovery testing platform on day one.

Start with three questions:

Can I connect?

If not, recovery has already failed.

Is the important data there?

Check critical tables, approximate row counts, and a few important queries.

Can I recover within my target?

Measure the actual restore time.

Then expand the test suite as you discover more failure modes.

The goal isn't to create a perfect replica of production.

The goal is to eliminate dangerous assumptions.

The uncomfortable question

Every engineering team should eventually be able to answer this:

If our production database disappeared right now, when was the last time we proved that our backup could become a working database?

If the answer is "we've never tested it," your backup system isn't necessarily broken.

But your confidence in it is an assumption.

And assumptions are exactly what disaster recovery is supposed to eliminate.

Backups are important.

Restores are the proof.


About Revenant

Revenant is an open-source CLI for PostgreSQL backup recovery validation. It supports local PostgreSQL databases and AWS RDS restore workflows, with YAML-based validation checks and JSON/Markdown reports.

The project is available on GitHub:

Revenant CLI on GitHub

For teams using GitHub Actions, there is also a dedicated action:

Revenant GitHub Action

Top comments (1)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.