Monitor Litestream SQLite Backups
Updated
Litestream continuously replicates a SQLite database to S3-compatible storage, a file system or another replica target. That makes it different from a nightly backup script: there is no cron job that finishes and could report success. Replication just keeps running, until a credential expires, the bucket policy changes, the disk fills up or the process is not restarted after a deploy. The database keeps working the whole time, so nobody notices until they need a restore. This guide sets up a scheduled restore test that proves the replica is usable and reports to Cuckoo.
How Cuckoo sees a continuous process
Since Litestream itself has no "run", you monitor the outcome instead: a small verification job runs on a schedule, restores the latest replica to a temporary file, checks it, and pings:
https://cuckoo.metastic.ch/ping/YOUR-UUID/startbefore the restore,https://cuckoo.metastic.ch/ping/YOUR-UUID/0if the restored copy is intact and recent,https://cuckoo.metastic.ch/ping/YOUR-UUID/1(or another non-zero code) if any check failed.
If the verification job itself stops running, no ping arrives and the check goes down after the grace time. One check therefore covers replication, the storage target and the verification job.
Step 1: confirm your Litestream version and commands
The CLI changed with v0.5, which moved to the LTX file format. Check what you have:
litestream version
On v0.5.x, litestream ltx lists the LTX files of a database and litestream restore recovers it. The older
snapshots and generations commands from v0.3 are not part of the current CLI. The examples below use v0.5
flags:
-o PATHwrites the restored database to a different path,-config PATHpoints to the config file (default/etc/litestream.yml),-if-replica-existsexits 0 when no backup exists (do not use it here; a missing backup should fail),-integrity-check quick|fullruns a SQLite check after restoring (added in v0.5.10).
Step 2: the verification script
Save as /usr/local/bin/litestream-verify and make it executable:
#!/usr/bin/env bash
set -uo pipefail
DB=/var/lib/app/app.db # path as configured in litestream.yml
HB=https://cuckoo.metastic.ch/ping/YOUR-UUID
MAX_AGE_SECONDS=3600 # newest restored row must be younger than this
tmp=$(mktemp -d)
log="$tmp/verify.log"
trap 'rm -rf "$tmp"' EXIT
curl -fsS -m 10 --retry 5 -o /dev/null "$HB/start" || true
verify() {
litestream restore -config /etc/litestream.yml -o "$tmp/restore.db" "$DB" || return 10
result=$(sqlite3 "$tmp/restore.db" 'PRAGMA integrity_check;') || return 11
[ "$result" = "ok" ] || { echo "integrity_check: $result"; return 12; }
# Freshness: adapt the query to a table your app writes to regularly
newest=$(sqlite3 "$tmp/restore.db" "SELECT CAST(strftime('%s', MAX(created_at)) AS INTEGER) FROM events;") || return 13
age=$(( $(date +%s) - ${newest:-0} ))
echo "newest row is ${age}s old"
[ "$age" -le "$MAX_AGE_SECONDS" ] || return 14
}
verify >"$log" 2>&1
code=$?
tail -c 10000 "$log" | curl -fsS -m 10 --retry 5 --data-binary @- -o /dev/null "$HB/$code" || true
exit $code
What each part catches:
litestream restorefails if the replica is missing, the credentials do not work or files are corrupt. On v0.5.10 and later you can add-integrity-check fullto the restore command as well.PRAGMA integrity_checkreturns the single rowokfor a healthy database. Anything else is reported.- The freshness query catches the quiet failure where old data restores fine but replication stopped
hours ago. Use a timestamp column your application updates often. If the app is idle at night, use a larger
MAX_AGE_SECONDSor a small table the app touches on a timer.
Distinct return codes (10 to 14) make the failed step visible in the Cuckoo dashboard, and the log tail is stored with the ping.
To restore straight from a replica URL without a config file, pass the URL instead of the database path, for
example litestream restore -o "$tmp/restore.db" s3://my-bucket/app.db, with the credentials in environment
variables.
Step 3: schedule it
With a systemd timer:
# /etc/systemd/system/litestream-verify.service
[Unit]
Description=Verify Litestream replica
[Service]
Type=oneshot
ExecStart=/usr/local/bin/litestream-verify
# /etc/systemd/system/litestream-verify.timer
[Unit]
Description=Hourly Litestream replica check
[Timer]
OnCalendar=hourly
RandomizedDelaySec=300
Persistent=true
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now litestream-verify.timer
Run the verification on a different machine from the database if you can. Then it also tests that the replica is reachable from somewhere else, which is the situation you are in during a real restore.
Schedule and grace advice
For an hourly timer, use a period of 1 hour and a grace time of 15 to 30 minutes, covering the random delay and the restore time. Restore time grows with the database size, so time a manual run first. Set max runtime slightly above the longest restore you expect so a hung download is reported.
Troubleshooting
- Exit code 10: the restore failed. Run the restore command manually and check credentials, bucket name and
litestream ltx /var/lib/app/app.dboutput. - Exit code 14 only: restore works, but data is stale. Check
systemctl status litestreamon the database host and look for upload errors in its logs. - No pings: check
systemctl list-timers litestream-verify.timerand thatsqlite3is installed.
FAQ
Why not monitor the Litestream process with systemd instead?
A running process does not prove that uploads succeed. Process monitoring is useful, but a restore test checks the result you actually depend on: a recent, intact copy you can restore.
How often should I run the restore test?
Hourly is a common balance between detection time and the cost of downloading the replica. Very large databases may need a daily full test combined with a more frequent, cheaper freshness check.
Does the restore test affect the live database?
No. It reads only from the replica and writes to a temporary directory that is deleted afterwards. The live database and the running Litestream process are not touched.
Related guides
- Borg Backup MonitoringMonitor BorgBackup: a script for borg create, prune and compact, how borg exit codes 0, 1 and 2 work, and borgmatic's healthchecks hook pointed at Cuckoo.
- Monitor pg_dump BackupsMonitor PostgreSQL pg_dump backups: custom-format dumps, pipefail, a size check, upload and a fail ping when the dump looks too small, plus a mysqldump note.
- Restic Backup MonitoringMonitor restic backups with one script for backup, forget --prune and check, exit codes like 3 for incomplete snapshots, an env file and heartbeat pings.
- Cron Job Start and Fail SignalsSend /start, success and /fail pings from cron jobs to measure run time and catch hung jobs. Includes a reusable bash wrapper script with trap and exit codes.