How to monitor Supabase backups and restore drills with Prometheus

If you already run Prometheus, your Supabase backups can report there like the rest of your infrastructure. From version 2.6.0, the open-source backupdrill CLI has an exporter command. Each time Prometheus scrapes it, it reads your backup bucket and reports when the last complete backup finished, how big it was, and whether the last restore drill passed. Below: setup, the metrics, and alert rules you can paste.

The short version

Needs
backupdrill CLI 2.6.0 or newer
Command
backupdrill exporter, serving http://127.0.0.1:10059/metrics (--host and --port change it)
Reads
Only the backup bucket, on every scrape. A key that can list and read the bucket is enough
Drill results
Run drills with --record, which leaves one small JSON file per drill in the bucket
Bucket unreachable
/metrics still answers, with backupdrill_up 0 and the reason in the exporter's log

Checked October 20, 2026.

What it reports

One exporter watches one project’s backups in your bucket. A backup counts once its manifest.json exists, because the CLI writes that file last, so a backup that is still running or died halfway doesn’t move the timestamp. Here is a full response:

backupdrill_up 1
backupdrill_scrape_duration_seconds 0.021105
backupdrill_build_info{version="2.6.0"} 1
backupdrill_snapshots 1
backupdrill_backup_last_success_timestamp_seconds 1792430998.015
backupdrill_latest_snapshot_dump_bytes 6822
backupdrill_latest_snapshot_storage_files 0
backupdrill_latest_snapshot_storage_bytes 0
backupdrill_latest_snapshot_tables 2
backupdrill_latest_snapshot_estimated_rows 520
backupdrill_drill_last_run_timestamp_seconds 1792431000.002
backupdrill_drill_last_run_passed 1
backupdrill_drill_last_success_timestamp_seconds 1792431000.002
backupdrill_drill_last_success_restore_duration_seconds 0.1
From a test run against a local Postgres 17 and an S3-compatible bucket: one database-only snapshot with 520 rows and one recorded drill that passed. HELP and TYPE lines are cut.
backupdrill_up1 if the bucket was read, 0 if not. The reason goes to the exporter's log.
backupdrill_scrape_duration_secondsHow long reading the bucket took.
backupdrill_build_info{version}The CLI version running the exporter. Always 1.
backupdrill_snapshotsSnapshot folders for the project, complete or not.
backupdrill_backup_last_success_timestamp_secondsWhen the newest complete snapshot finished, from createdAt in its manifest.json. 0 if there is none.
backupdrill_latest_snapshot_dump_bytesSize of the database dump in that snapshot.
backupdrill_latest_snapshot_storage_filesStorage files in that snapshot. 0 for a database-only backup.
backupdrill_latest_snapshot_storage_bytesTotal size of those Storage files.
backupdrill_latest_snapshot_tablesTables captured in that snapshot.
backupdrill_latest_snapshot_estimated_rowsRow count Postgres estimated at backup time (from its table statistics).
backupdrill_drill_last_run_timestamp_secondsWhen the last recorded drill finished, passed or not. 0 if none is recorded.
backupdrill_drill_last_run_passed1 if that drill passed, 0 if not.
backupdrill_drill_last_success_timestamp_secondsWhen the last recorded drill that passed finished. 0 if none is recorded.
backupdrill_drill_last_success_restore_duration_secondsHow long the database restore took in that drill.

Everything is a gauge, and timestamps are Unix seconds, so time() - metric gives an age. While the bucket has no complete snapshot, the latest_snapshot metrics are left out and the backup timestamp is 0, which makes an age-based alert fire. backupdrill_drill_last_run_passed shows up once a drill has been recorded, and the restore duration once a recorded drill has passed.

Set it up

This assumes your backups already run with the backupdrill CLI (MIT, github.com/backupdrill/cli) from cron or GitHub Actions. If not, start with the CLI docs. Check backupdrill --version first: older versions don’t have the exporter command or --record.

Step 1 — run the exporter

The exporter reads the same bucket settings as backup and drill (environment variables, a config file or flags) and doesn’t need the database connection string. It never writes to the bucket, so give it its own key that can only list and read.

export BACKUPDRILL_S3_ENDPOINT="https://<account>.r2.cloudflarestorage.com"  # omit for AWS S3
export BACKUPDRILL_S3_REGION="auto"
export BACKUPDRILL_S3_BUCKET="my-backups"
export BACKUPDRILL_S3_ACCESS_KEY_ID="…"       # a key that can list and read the bucket
export BACKUPDRILL_S3_SECRET_ACCESS_KEY="…"
export BACKUPDRILL_PROJECT_NAME="my-app"      # the same name your backups use

npx -y backupdrill exporter                   # http://127.0.0.1:10059/metrics

It listens on 127.0.0.1:10059. If Prometheus runs on another machine, add --host 0.0.0.0 and let only the Prometheus server reach that port: every request to /metrics turns into requests against your bucket, so it shouldn’t be open to the internet. The process stays in the foreground, so run it the way you run your other exporters, for example as a systemd service. For a second project, start a second exporter with --project-name and another --port.

Step 2 — record your restore drills

backupdrill drill restores the newest snapshot into a throwaway Postgres in Docker and checks it. Normally it only reads the bucket and leaves nothing behind, so the exporter has nothing to report. Add --record to the drill you run on a schedule:

# crontab -e, on a machine with Docker: every Sunday at 04:00
0 4 * * 0 . "$HOME/.backupdrill.env" && npx -y backupdrill drill --record >> "$HOME/backupdrill-drill.log" 2>&1

When the drill ends, it writes one small JSON file to <prefix>/<project>.drills/, next to the project’s snapshot folder, named after the time it finished, the result and a random tag, for example 2026-10-20T04-12-09-311Z-passed-3f9a1c07.json. The file holds the time, whether it passed, the snapshot name and the restore time. The check details are left out on purpose, because a failed check can quote rows from your tables. Each drill writes a file under its own name instead of updating a shared one, so it doesn’t matter which of two overlapping drills finishes writing first. A drill that stops early, say Docker isn’t running, is recorded as not passed. This needs a key that can write to the bucket. If the write fails, the drill prints a warning and its own result and exit code stay the same.

Step 3 — add a scrape job

scrape_configs:
  - job_name: backupdrill
    scrape_interval: 1m
    static_configs:
      - targets: ["backup-host:10059"]
        labels:
          project: my-app

A minute is plenty for numbers that change once a day, and it stays well inside the 5 minutes Prometheus looks back for a sample. The project label is yours to set; the exporter doesn’t add one.

Step 4 — add alert rules

Five rules cover the ways this goes wrong. Change the 26 hours to 8 days if you back up weekly.

groups:
  - name: backupdrill
    rules:
      - alert: BackupdrillBucketUnreadable
        expr: backupdrill_up == 0
        for: 15m
        annotations:
          summary: "The backupdrill exporter can't read the backup bucket (see its log)"
      - alert: SupabaseBackupMissing
        # daily backups; use 8 * 86400 for weekly
        expr: time() - backupdrill_backup_last_success_timestamp_seconds > 26 * 3600
        for: 15m
        annotations:
          summary: "No complete Supabase backup in the last 26 hours"
      - alert: SupabaseBackupShrank
        expr: backupdrill_latest_snapshot_dump_bytes < 0.7 * max_over_time(backupdrill_latest_snapshot_dump_bytes[7d])
        annotations:
          summary: "The newest database dump is over 30% smaller than the largest one this week"
      - alert: SupabaseRestoreDrillFailed
        expr: backupdrill_drill_last_run_passed == 0
        annotations:
          summary: "The last restore drill did not pass"
      - alert: SupabaseRestoreDrillMissing
        # weekly drills
        expr: time() - backupdrill_drill_last_success_timestamp_seconds > 8 * 86400
        for: 1h
        annotations:
          summary: "No passing restore drill in the last 8 days"

Save them as backupdrill-alerts.yml next to prometheus.yml, list the file under rule_files, and reload Prometheus (send it a SIGHUP, or POST to /-/reload if it runs with --web.enable-lifecycle). Until the file is listed there, Prometheus doesn’t evaluate any of these rules.

# prometheus.yml
rule_files:
  - backupdrill-alerts.yml
  • BackupdrillBucketUnreadable catches a rotated key or a bucket that moved. Without it, the other rules go quiet when the exporter can’t read anything. If the exporter process itself stops, Prometheus’s own up metric for the job drops to 0, which most setups already alert on.
  • SupabaseBackupMissing fires whether the backup job failed, never ran, or ran against a different prefix. It only looks at what is in the bucket.
  • SupabaseBackupShrank is for the backup that “succeeds” with most of the data missing, for example after a wrong schema setting or a dropped table.
  • SupabaseRestoreDrillFailed and SupabaseRestoreDrillMissing cover drills: one that ran and failed, and drills that stopped passing or stopped running.

Step 5 — check it

curl -s http://127.0.0.1:10059/metrics | grep -E '^backupdrill_(up|snapshots) '
promtool check rules backupdrill-alerts.yml

You should see backupdrill_up 1 and your snapshot count. If backupdrill_up is 0, the exporter’s log says why, for example Could not read the backup bucket: Access Denied. On the Prometheus side, the target should show as up on the Targets page within a minute, and the five rules should be listed on the Rules page.

What each scrape does

The exporter doesn’t keep a cache of its own numbers. On every scrape it lists the snapshot folders and the drill files (one request per 1,000 of each), reads the newest manifest.json, and reads the newest passing drill file for its restore time. The manifest is fetched with If-None-Match, so when it hasn’t changed the bucket answers 304 Not Modified and nothing is downloaded. That matters once you back up Storage files, because the manifest lists every one of them.

At a one-minute interval that comes to about 86,000 list requests and 86,000 small reads a month; check your storage provider’s request pricing. Each read stops half a second before the scrape timeout Prometheus sends with the request (10 seconds when there is none), so a slow bucket shows up as backupdrill_up 0 instead of a failed scrape.

Timestamps from the future get special handling, because they would hold back the alerts that work out an age from them. A manifest dated more than 10 minutes ahead counts as a failed read, and a drill file named with such a time is skipped with a warning in the log. Either usually means the clock is wrong on the machine that wrote it.

Why it reads the bucket instead of pushing

For batch jobs, Prometheus suggests pushing a last-success timestamp to a Pushgateway, and that would catch a backup job that stops running too. The exporter checks the bucket on each scrape instead, so it also notices when snapshots disappear after a backup finished, for example because a lifecycle rule deleted them too early. A timestamp pushed by the job wouldn’t change. You also don’t need to run a Pushgateway.

FAQ

Does the exporter need my database connection string?

No. It only reads the backup bucket: the list of snapshot folders and drill files, the newest manifest.json, and the newest passing drill file. It never connects to Supabase, so it can run on a machine that has no access to your database.

Can one exporter watch several Supabase projects?

No, one exporter covers one project. Run another with --project-name and a different --port, and give each scrape target its own project label. The exporter doesn't add a project label, so the label in the scrape config is what tells the projects apart.

Why does the backup timestamp ignore my newest snapshot folder?

Because it has no manifest.json yet. The CLI writes the manifest last, so a folder without one is a backup that is still running or one that failed after uploading the dump. The exporter counts the folder in backupdrill_snapshots and reports the newest complete snapshot instead.

Do I need this with the hosted BackupDrill service?

No. The hosted service schedules backups and restore drills itself and emails you when one fails. The exporter is for running the open-source CLI on your own schedule and keeping the alerts in Prometheus.

Sources

  • Prometheus docs — Writing exporters
  • Prometheus docs — Metric and label naming
  • Prometheus docs — Instrumentation: batch jobs
  • Prometheus docs — Alerting rules
  • backupdrill CLI — README

Facts and prices last verified October 20, 2026 against the sources above. Written by the team behind BackupDrill.

If you’d rather not schedule the backup and drill jobs yourself, the hosted BackupDrill service runs the same engine for you and emails you when a backup or drill fails. Free covers weekly backups for one project; Solo adds weekly restore drills. Start free, or see the CLI docs to keep running the CLI on your own.