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| Metric | What it tells you |
|---|---|
| backupdrill_ | 1 if the bucket was read, 0 if not. The reason goes to the exporter's log. |
| backupdrill_ | How long reading the bucket took. |
| backupdrill_ | The CLI version running the exporter. Always 1. |
| backupdrill_ | Snapshot folders for the project, complete or not. |
| backupdrill_ | When the newest complete snapshot finished, from createdAt in its manifest.json. 0 if there is none. |
| backupdrill_ | Size of the database dump in that snapshot. |
| backupdrill_ | Storage files in that snapshot. 0 for a database-only backup. |
| backupdrill_ | Total size of those Storage files. |
| backupdrill_ | Tables captured in that snapshot. |
| backupdrill_ | Row count Postgres estimated at backup time (from its table statistics). |
| backupdrill_ | When the last recorded drill finished, passed or not. 0 if none is recorded. |
| backupdrill_ | 1 if that drill passed, 0 if not. |
| backupdrill_ | When the last recorded drill that passed finished. 0 if none is recorded. |
| backupdrill_ | How 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/metricsIt 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>&1When 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-appA 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
upmetric 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.ymlYou 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.