Litestream continuously replicates your SQLite database to S3, GCS, or Azure Blob Storage by streaming WAL changes in real time. It is the backbone of disaster recovery for SQLite-backed applications — including Elixir/Phoenix apps using Ecto with the SQLite adapter — but Litestream itself runs as a sidecar process that can silently fall behind, crash, or lose its connection to object storage without producing any obvious application error. Vigilmon monitors the Litestream process and your replication health from the outside so you know before a disaster that your backup is still working.
In this guide you'll build a health endpoint that exposes Litestream replication state, set up Vigilmon HTTP and heartbeat monitors, and wire in alerting so your on-call team is notified the moment replication falls behind or the process dies.
What You'll Build
- A
/health/litestreamendpoint that reports replication lag and last checkpoint time - A Vigilmon HTTP monitor that checks the endpoint every minute
- A Vigilmon heartbeat for Litestream's background replication loop
- A restore-verification cron job that proves your backup can actually be restored
- Email and Slack alert channels
Prerequisites
- Litestream installed (
brew install benbjohnson/litestream/litestreamorapt install litestream) - A SQLite-backed application (Elixir/Phoenix + Ecto with SQLite, or any other stack)
- Object storage configured in
litestream.yml(S3, GCS, or Azure Blob) - A free Vigilmon account
Step 1: Enable Litestream Metrics
Litestream exposes Prometheus metrics when you set addr in your config:
# litestream.yml
addr: "0.0.0.0:9090" # metrics and health endpoint
dbs:
- path: /data/app.db
replicas:
- type: s3
bucket: my-app-backups
path: app-db
region: us-east-1
access-key-id: $AWS_ACCESS_KEY_ID
secret-access-key: $AWS_SECRET_ACCESS_KEY
With addr set, Litestream exposes:
GET /metrics— Prometheus metrics including replication lag and WAL sizeGET /— HTML page with replica status
Verify it works:
litestream replicate -config litestream.yml &
curl -s http://localhost:9090/metrics | grep litestream
Key metrics:
| Metric | Description |
|---|---|
| litestream_replica_wal_index | Current WAL index being replicated |
| litestream_replica_sync_count_total | Total sync operations completed |
| litestream_replica_operation_bytes_total | Bytes uploaded to object storage |
| litestream_db_wal_size | Current WAL file size in bytes |
Step 2: Build a Health Endpoint
Litestream's built-in / page is HTML, not machine-readable. Wrap the metrics in a purpose-built health endpoint so Vigilmon can assert on its response body.
For Elixir/Phoenix — add a controller:
# lib/my_app_web/controllers/litestream_health_controller.ex
defmodule MyAppWeb.LitestreamHealthController do
use MyAppWeb, :controller
@metrics_url "http://localhost:9090/metrics"
@max_lag_seconds 300 # alert if replication is more than 5 minutes behind
def check(conn, _params) do
with {:ok, %{body: body, status: 200}} <- fetch_metrics(),
{:ok, lag} <- parse_lag(body) do
if lag <= @max_lag_seconds do
json(conn, %{status: "ok", replication_lag_seconds: lag})
else
conn
|> put_status(503)
|> json(%{status: "degraded", replication_lag_seconds: lag,
message: "Replication lag exceeds threshold"})
end
else
_ ->
conn
|> put_status(503)
|> json(%{status: "error", message: "Cannot reach Litestream metrics endpoint"})
end
end
defp fetch_metrics do
:httpc.request(:get, {@metrics_url |> String.to_charlist(), []}, [], [])
|> case do
{:ok, {{_, status, _}, _headers, body}} ->
{:ok, %{status: status, body: to_string(body)}}
error -> error
end
end
defp parse_lag(metrics_body) do
# litestream_replica_wal_index{db=..., name=...} 42
# Use the WAL index age as a proxy for lag — compare current time vs last sync
# A simpler check: verify the process is running and syncing
case Regex.scan(~r/litestream_replica_sync_count_total\{[^}]+\}\s+(\d+)/, metrics_body) do
[[_, count] | _] -> {:ok, String.to_integer(count)}
_ -> {:error, :no_metrics}
end
end
end
Add the route in router.ex:
scope "/health", MyAppWeb do
get "/litestream", LitestreamHealthController, :check
end
For a standalone script (language-agnostic) — serve a health endpoint with Python:
#!/usr/bin/env python3
# litestream_health_server.py
import re
import time
import urllib.request
from http.server import HTTPServer, BaseHTTPRequestHandler
import json
METRICS_URL = "http://localhost:9090/metrics"
MAX_LAG_BYTES = 10 * 1024 * 1024 # 10 MB WAL backlog = degraded
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
if self.path != "/health/litestream":
self.send_response(404)
self.end_headers()
return
try:
with urllib.request.urlopen(METRICS_URL, timeout=5) as r:
body = r.read().decode()
wal_match = re.search(r"litestream_db_wal_size\S*\s+([\d.]+)", body)
wal_size = float(wal_match.group(1)) if wal_match else None
healthy = wal_size is not None and wal_size < MAX_LAG_BYTES
status = "ok" if healthy else "degraded"
payload = json.dumps({"status": status, "wal_size_bytes": wal_size}).encode()
self.send_response(200 if healthy else 503)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(payload)
except Exception as e:
payload = json.dumps({"status": "error", "message": str(e)}).encode()
self.send_response(503)
self.send_header("Content-Type", "application/json")
self.end_headers()
self.wfile.write(payload)
def log_message(self, *args):
pass
HTTPServer(("0.0.0.0", 9101), Handler).serve_forever()
Test it:
python3 litestream_health_server.py &
curl -s http://localhost:9101/health/litestream | jq .
# {"status": "ok", "wal_size_bytes": 4096.0}
Step 3: Set Up the Vigilmon HTTP Monitor
- Log in to Vigilmon and click New Monitor → HTTP.
- Enter
http://your-server:9101/health/litestream(orhttps://your-domain.com/health/litestreamif behind a reverse proxy). - Set check interval to 60 seconds.
- Add assertions:
- Status code equals
200 - Response body contains
"status":"ok"
- Status code equals
- Save the monitor.
Vigilmon probes from multiple regions; two consecutive failures trigger your alert channels.
Step 4: Set Up a Heartbeat Monitor for Litestream
The HTTP health check verifies that Litestream responds, but it doesn't verify that replication is actively progressing. Set up a heartbeat that only pings Vigilmon after a confirmed replication event.
- In Vigilmon, go to New Monitor → Heartbeat.
- Set expected interval to 10 minutes.
- Copy the ping URL (e.g.,
https://vigilmon.online/ping/YOUR_HEARTBEAT_ID).
Write a script that checks replication progress and pings the heartbeat:
#!/usr/bin/env bash
# /usr/local/bin/litestream-heartbeat.sh
HEARTBEAT_URL="https://vigilmon.online/ping/YOUR_HEARTBEAT_ID"
METRICS_URL="http://localhost:9090/metrics"
# Get current WAL index
wal_index=$(curl -sf "$METRICS_URL" | \
grep 'litestream_replica_wal_index' | \
grep -v '^#' | \
awk '{print $2}' | head -1)
if [[ -n "$wal_index" && "$wal_index" -gt 0 ]]; then
curl -sf "$HEARTBEAT_URL" > /dev/null
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) heartbeat sent (wal_index=$wal_index)"
else
echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) heartbeat withheld — replication not progressing"
fi
Add to cron:
chmod +x /usr/local/bin/litestream-heartbeat.sh
crontab -e
# Add:
# */10 * * * * /usr/local/bin/litestream-heartbeat.sh >> /var/log/litestream-heartbeat.log 2>&1
If Litestream crashes or WAL replication stalls, the heartbeat stops and Vigilmon alerts after the expected interval passes.
Step 5: Restore Verification Cron Job
Monitoring that Litestream is replicating is necessary but not sufficient — you also need to verify that the replicated data can actually be restored. Add a daily restore verification:
#!/usr/bin/env bash
# /usr/local/bin/litestream-verify-restore.sh
set -euo pipefail
RESTORE_PATH="/tmp/litestream-verify-$(date +%s).db"
SOURCE_DB="/data/app.db"
HEARTBEAT_URL="https://vigilmon.online/ping/YOUR_RESTORE_VERIFY_HEARTBEAT_ID"
cleanup() { rm -f "$RESTORE_PATH"; }
trap cleanup EXIT
# Attempt restore from object storage
litestream restore \
-config /etc/litestream.yml \
-replica s3 \
-o "$RESTORE_PATH" \
"$SOURCE_DB"
# Verify the restored database is readable
row_count=$(sqlite3 "$RESTORE_PATH" "SELECT COUNT(*) FROM sqlite_master;")
if [[ "$row_count" -ge 0 ]]; then
curl -sf "$HEARTBEAT_URL" > /dev/null
echo "$(date -u) Restore verified OK (sqlite_master rows: $row_count)"
else
echo "$(date -u) Restore verification FAILED — database not readable"
exit 1
fi
Create a separate Vigilmon heartbeat with a 24-hour interval for restore verification. Schedule the script as a daily cron:
chmod +x /usr/local/bin/litestream-verify-restore.sh
# Run at 03:00 UTC daily
echo "0 3 * * * root /usr/local/bin/litestream-verify-restore.sh >> /var/log/litestream-verify.log 2>&1" \
>> /etc/cron.d/litestream-verify
Step 6: Supervise the Litestream Process
Litestream should run under a process supervisor so it restarts automatically on crash. Use systemd:
# /etc/systemd/system/litestream.service
[Unit]
Description=Litestream SQLite Replication
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
User=app
ExecStart=/usr/bin/litestream replicate -config /etc/litestream.yml
Restart=on-failure
RestartSec=5s
StandardOutput=journal
StandardError=journal
# Environment for S3 credentials
EnvironmentFile=/etc/litestream.env
[Install]
WantedBy=multi-user.target
Enable and start:
systemctl daemon-reload
systemctl enable --now litestream
systemctl status litestream
Verify the process is running and metrics are available:
curl -s http://localhost:9090/metrics | grep -c litestream
Step 7: Configure Alert Channels
In Vigilmon under Alerts → Channels:
Email — add your on-call address. You'll receive an alert within 30 seconds of two consecutive HTTP check failures and a recovery notification when the check passes again.
Slack — click Add Channel → Slack, paste your incoming webhook URL. Route CRITICAL monitors to both email and Slack under Alerts → Routing.
Create two alert policies:
- Litestream process health (HTTP monitor): notify immediately on first failure
- Restore verification (heartbeat): notify if restore hasn't been verified in 25 hours
Step 8: Verify End-to-End
- Confirm the Vigilmon dashboard shows your HTTP monitor as UP.
- Stop the Litestream process:
systemctl stop litestream— verify Vigilmon fires an alert. - Restart:
systemctl start litestream— verify you receive a recovery notification. - Trigger the restore verification script manually and confirm the heartbeat is received in the Vigilmon dashboard.
Production Checklist
- [ ]
addrset inlitestream.ymlto expose Prometheus metrics - [ ] Health endpoint (
/health/litestream) returning200with JSON body - [ ] Vigilmon HTTP monitor with status code + body assertions
- [ ] Heartbeat monitor (10-minute interval) for replication progress
- [ ] Heartbeat monitor (24-hour interval) for restore verification
- [ ] Litestream running under systemd with
Restart=on-failure - [ ] Daily restore verification cron job
- [ ] Email and Slack alert channels configured
Summary
You now have Litestream monitored end-to-end with Vigilmon:
- HTTP monitor verifying the Litestream process is alive and WAL replication is not backed up
- Heartbeat confirming replication is actively progressing every 10 minutes
- Daily restore verification proving your backup can actually be recovered
- Email and Slack alerts firing within seconds of any failure
Litestream gives SQLite-backed applications S3-grade durability. Vigilmon makes sure Litestream itself never silently stops working.