Monitoring, alerting and backups
Know something is wrong before your users do, and be able to undo the worst day.
In 30 seconds
Monitoring tells you whether your system is healthy right now and why it is not when something breaks. Alerting turns that into a message to a human, but only when a human needs to act. Backups are your last line of defence against deleted data, ransomware or a dead server, and they only count if you have actually restored one.
Why I use it
- I set up monitoring because the worst way to learn about an outage is from a client's phone call.
- I use metrics, logs and error tracking together because each answers a different question: is it broken, what happened, and where in the code.
- I treat backups as worthless until a restore has been tested; a backup that has never been restored is a hope, not a plan.
- Good alerting protects people too: a team that gets paged for noise learns to ignore pages.
How it works
01Metrics, logs and traces: three views of one system
Metrics are numbers over time: requests per second, error rate, response time, CPU, free disk, queue length. They are cheap to store and perfect for dashboards and alerts, but they tell you that something is wrong, not exactly what. Logs are timestamped events with detail: this request, this user, this exception and stack trace. They answer what happened, but are expensive to search at scale, so structure them as JSON with consistent fields. Traces follow one request across services: the API call, the database queries, the call to the payment provider, each with its duration, so you can see where the time went. OpenTelemetry is the vendor-neutral standard for producing all three. Start with metrics and logs; add tracing when you have several services talking to each other.
- Metrics: is it healthy? (aggregated numbers)
- Logs: what exactly happened? (individual events)
- Traces: where did this request spend its time? (one request, many hops)
02Prometheus, Grafana and Loki
Prometheus is a time-series database that pulls (scrapes) metrics over HTTP from targets every few seconds. Exporters expose metrics for things you did not write: node_exporter for CPU, memory and disk, postgres_exporter for the database, and your own app can expose a /metrics endpoint. You query it with PromQL and define alert rules; Alertmanager then groups, deduplicates and routes those alerts to email, Slack or a pager. Grafana draws dashboards on top of Prometheus and many other sources. Loki stores logs cheaply by indexing only labels such as app, environment and level, not the full text, and Grafana Alloy is the collector that ships logs and metrics to it. Together they give a self-hosted stack that runs comfortably beside a Docker Compose application.
groups:
- name: host
rules:
- alert: DiskAlmostFull
expr: node_filesystem_avail_bytes{mountpoint="/"} / node_filesystem_size_bytes{mountpoint="/"} < 0.15
for: 15m
labels: { severity: warning }
annotations:
summary: "Less than 15% disk left on {{ $labels.instance }}"
- alert: HighErrorRate
expr: sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 0.05
for: 10m
labels: { severity: critical }
03Uptime checks, error tracking and alerts that do not spam
An uptime check probes your site from outside, every minute, the way a user would: does the homepage return 200, is the TLS certificate valid for more than two weeks, does the health endpoint confirm the database answers. Tools like Uptime Kuma can be self-hosted; hosted checkers test from several regions. Error tracking (Sentry for web and backend, Firebase Crashlytics for mobile) groups exceptions and crashes, shows the stack trace, release and affected users. For alerts, page a human only for symptoms users feel (site down, error rate high, checkout failing) and send causes like high CPU to a dashboard or a low-priority channel. Require a condition to last several minutes before firing, group related alerts, and every alert should link to a runbook explaining what to check first.
- Alert on symptoms, investigate causes.
- Every page must be actionable; if nobody needs to act, it is not a page.
- Review noisy alerts monthly and delete or tune them.
04Backups: the 3-2-1 rule, snapshots vs real backups
The 3-2-1 rule: keep three copies of your data, on two different types of storage, with one copy off-site. A modern extension adds one copy that is immutable or offline, so ransomware or a stolen admin key cannot delete it, and zero errors after a verified restore. A provider snapshot copies the whole disk at a point in time. It is fast to restore a whole server, but it usually lives in the same provider account, and a snapshot of a running database can be inconsistent. A real backup is a consistent, application-level copy, such as pg_dump or mysqldump output plus uploaded files, stored somewhere else and encrypted. Use both: snapshots for fast full-server recovery, logical backups for precise, portable restores of data.
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F_%H%M)
pg_dump -Fc -U app appdb > /backups/appdb_$STAMP.dump
# encrypted, deduplicated, off-site copy (S3-compatible storage)
export RESTIC_REPOSITORY=s3:https://s3.example.com/your-backup-bucket
restic backup /backups /var/www/app/shared/storage
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune
restic check
05Testing restores, RPO and RTO
Two numbers define a backup strategy, and they should be agreed with the business, not guessed by engineers. RPO, recovery point objective, is how much data you can afford to lose, measured in time: with nightly dumps your RPO is up to 24 hours; with continuous WAL archiving or binlog shipping it can be minutes. RTO, recovery time objective, is how long you can be down while restoring. Then prove you can meet them. On a schedule, restore the latest backup into a scratch database or a temporary server, run a few sanity queries, check uploaded files open, and time the whole process. Write the steps down as a runbook so anyone on the team can do it under stress.
# monthly restore drill into a throwaway database
createdb restore_test
pg_restore --no-owner -d restore_test /backups/appdb_latest.dump
psql -d restore_test -c "SELECT count(*), max(created_at) FROM orders;"
dropdb restore_test
Use it when
- From day one of anything in production: at least uptime checks, error tracking and automated off-site backups.
- Full metrics and log stacks once you run your own servers or several services.
- Tracing when requests cross several services or queues and slowness is hard to locate.
- Restore drills before any big migration and on a regular schedule after that.
Skip it when
- A self-hosted Prometheus and Loki stack for one small site; a hosted uptime checker and error tracker are enough.
- Paging alerts for every metric spike; most signals belong on a dashboard, not on someone's phone.
- Relying only on snapshots stored in the same account as the server.
Alternatives
- Hosted observability (Grafana Cloud, Datadog, New Relic)
No servers to run, faster to start; cost grows with data volume.
- Uptime Kuma or provider alerts
Simple uptime and disk or load alerts for small setups.
- Sentry / Crashlytics
Error and crash tracking with stack traces, releases and affected users.
- Managed database backups
Point-in-time recovery handled by the provider; still test restores and keep an external copy.
Best practices
- Monitor the four golden signals: latency, traffic, errors and saturation.
- Use structured JSON logs with a request ID that flows through every service and job.
- Page only on user-facing symptoms; link every alert to a runbook.
- Follow 3-2-1 with at least one immutable or separate-account copy, and encrypt backups.
- Back up uploaded files and configuration, not only the database.
- Schedule restore drills, time them, and compare with the agreed RPO and RTO.
- Alert when a backup job fails or has not run, not only when the site is down.
Common mistakes
- Keeping backups on the same server or in the same account as the data they protect.
- Never testing a restore and discovering a corrupt or empty dump during an incident.
- Alerting on everything until the team mutes the channel.
- Unrotated logs filling the disk and taking the site down.
- Logging passwords, tokens or full card data while debugging.
Deep dive
06SLIs, SLOs, error budgets and burn-rate alerts
A service level indicator (SLI) is a measured ratio of good events to all events, for example the share of requests answered successfully in under 500 ms. A service level objective (SLO) is the target for it over a window, say 99.9% over 30 days. The remaining 0.1% is the error budget: about 43 minutes of full outage per month, or a longer period of partial errors. Instead of alerting on a fixed error rate, alert on how fast the budget is burning. A burn rate of 1 uses exactly the budget in 30 days; a burn rate of 14.4 sustained for an hour consumes 2% of the monthly budget and deserves a page. The multiwindow pattern from Google's SRE workbook checks a long window to be sure the problem is real and a short window to be sure it is still happening, so alerts fire fast and clear fast.
# page: 2% of a 30-day 99.9% budget burned in 1 hour
- alert: ErrorBudgetFastBurn
expr: |
(sum(rate(http_requests_total{status=~"5.."}[1h])) / sum(rate(http_requests_total[1h])) > 14.4 * 0.001)
and
(sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) > 14.4 * 0.001)
labels: { severity: page }
annotations: { runbook: "https://wiki.example.com/runbooks/error-budget" }
07Prometheus internals: series, cardinality and histograms
Prometheus stores one time series for every unique combination of metric name and label values. http_requests_total with labels for method, route and status might produce a few hundred series, which is fine. Add a user_id or full URL label and you create one series per user or per URL, memory explodes and queries crawl; this is the cardinality problem, and it is the most common way to break Prometheus. Keep labels to small, bounded sets and put high-cardinality detail into logs or traces. For latency, use histograms: the app counts requests into buckets, and histogram_quantile estimates p95 or p99 at query time, across all instances. Averages hide the slow tail that users actually feel. Expensive queries used on dashboards and alerts should become recording rules, computed once per interval. Prometheus keeps 15 days by default; long-term storage usually goes to Thanos, Mimir or a hosted backend via remote_write.
# p95 latency per route over 5 minutes
histogram_quantile(0.95,
sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))
# recording rule: compute once, reuse in dashboards and alerts
- record: route:http_request_duration_seconds:p95_5m
expr: histogram_quantile(0.95, sum by (le, route) (rate(http_request_duration_seconds_bucket[5m])))
# find the metrics with the most series
topk(10, count by (__name__) ({__name__=~".+"}))
08Point-in-time recovery for PostgreSQL
A nightly dump gives an RPO of up to a day. Point-in-time recovery (PITR) brings it down to minutes. PostgreSQL writes every change to its write-ahead log (WAL) before applying it. If you keep a periodic base backup, a physical copy of the data directory, plus every WAL segment since then, you can restore the base backup and replay the WAL up to any moment, for example one minute before someone ran a DELETE without a WHERE clause. You enable it with archive_mode and an archive command that ships each finished segment off the server. In practice use a dedicated tool such as pgBackRest or WAL-G, which handle compression, encryption, retention, parallel restore and verification for you, and push to object storage. Restore by placing the base backup, setting recovery_target_time and creating a recovery.signal file. Managed databases offer the same feature as a button, but you still need to rehearse it.
# postgresql.conf (with pgBackRest)
wal_level = replica
archive_mode = on
archive_command = 'pgbackrest --stanza=app archive-push %p'
archive_timeout = 60 # ship at least once a minute
# restore to just before the mistake
pgbackrest --stanza=app --type=time \
--target="2026-11-05 14:31:00+00" --target-action=promote restore
09Protecting the backups themselves
Modern ransomware and malicious insiders go for the backups first, so assume an attacker with the server's credentials will try to delete them. Three defences work together. Separate credentials: the server should be able to add new backups but not delete old ones. restic's rest-server has an append-only mode, and object storage can grant write without delete. Immutability: S3 Object Lock (also offered by several S3-compatible providers) stores objects in write-once mode for a retention period; in compliance mode nobody, not even the account root, can shorten it. Independent monitoring: a dead man's switch expects a ping after every successful backup and alerts when one is missing, which catches silently broken cron jobs. Keep the encryption key or password somewhere separate from the server, such as a password manager, because an encrypted backup without its key is just noise.
# bucket must have versioning; lock new objects for 30 days
aws s3api put-object-lock-configuration --bucket example-backups \
--object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"COMPLIANCE","Days":30}}}'
# end of backup script: report success to a dead man's switch
restic backup /backups && curl -fsS -m 10 --retry 3 https://hc-ping.example.com/your-check-id
Real-world scenarios
Users say the site is slow, but no alert fired
Problem
Support receives complaints about slow pages at lunchtime. The dashboards show normal CPU and an average response time of 180 ms, and no alert has fired all week.
How to solve it
Averages hide the tail, so look at percentiles. Plot p95 and p99 latency per route from the request duration histogram; usually one or two routes are slow for a fraction of users. Check saturation next: database connection pool usage, slow query log, queue backlog and worker counts at lunchtime. If several services are involved, open a trace for a slow request and see which span dominates. Fix the cause, often a missing index or an N+1 query that only hurts at peak. Then add a latency SLO on p95 for the key routes with a burn-rate alert, so the next slowdown pages someone before support hears about it.
A table was emptied by mistake at 14:32
Problem
A maintenance script ran a DELETE without the intended filter on a production table. The app keeps working, but hundreds of customers have lost their records and new orders keep arriving.
How to solve it
Do not restore over production, because you would lose every order written since. With PITR, restore the latest base backup plus WAL to a separate server with a recovery target of 14:31, one minute before the mistake. With only a nightly dump, restore it to a scratch database instead. Then copy back only the deleted rows, for example with a COPY of the missing IDs or an INSERT ... SELECT across a temporary foreign data wrapper, checking foreign keys. Communicate status to stakeholders while you work. Afterwards, add guards: scripts run in a transaction with a row-count check, and dangerous operations need review.
The team receives 200 alerts a week and ignores them
Problem
The alert channel is muted by most of the team. A real outage last month was noticed forty minutes late because its alert was buried among warnings about CPU spikes.
How to solve it
Export a month of alert history and sort by count and by whether anyone acted. Delete alerts that never led to action, and move cause-based ones such as CPU or memory to dashboards or a daily digest. Keep paging only for user-facing symptoms: uptime, error budget burn, latency SLOs, failed backups. Add for: durations so short spikes do not fire, group related alerts in Alertmanager, and use inhibition so a host-down alert suppresses the twenty alerts it causes. Give every remaining alert an owner and a runbook link. Review the list monthly; a reasonable target is a few actionable pages per week.
Cheat sheet
- Validate alert and recording rules
promtool check rules alerts.yml- Silence an alert during maintenance
amtool silence add alertname=DiskAlmostFull --duration=2h --comment="disk resize"- p95 latency (PromQL)
histogram_quantile(0.95, sum by (le) (rate(http_request_duration_seconds_bucket[5m])))- Error ratio (PromQL)
sum(rate(http_requests_total{status=~"5.."}[5m])) / sum(rate(http_requests_total[5m]))- Errors in logs (LogQL)
sum by (app) (count_over_time({env="prod"} |= "ERROR" [5m]))- Restore a dump in parallel
pg_restore -j 4 --no-owner -d restore_test appdb.dump- List and restore restic snapshots
restic snapshots && restic restore latest --target /tmp/restore- Find what fills the disk
du -xh / --max-depth=2 2>/dev/null | sort -h | tail -20- Shrink the systemd journal
journalctl --vacuum-size=500M- Check TLS expiry from outside
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -enddate
In my real projects
- The POS SaaS server runs a Prometheus, Grafana and Loki observability stack as a Compose override, with health checks, runbooks and backup scripts.
- The React Native apps report crashes through Firebase Crashlytics.
- My server baseline includes snapshots plus tested restores, and monitoring of disk, load and uptime through Uptime Kuma or provider alerts; off-site backups are part of my current consolidation plan.
- I cleaned one production server from about 91% disk usage down to about 66% by removing old logs, old builds and oversized node_modules.
Questions & answers
Q1What is the difference between metrics, logs and traces?
Metrics are numeric time series, like request rate, error rate, latency percentiles or free disk. They are cheap, easy to aggregate and ideal for dashboards and alerts, but they lose individual detail. Logs are discrete events with context, such as one failed request with its user, input and stack trace; they explain what happened but are costly to store and search. Traces follow a single request across services and show each step with its timing, which is how you find where latency comes from in a distributed system. In practice I alert on metrics, then use logs and traces, linked by a request ID, to find the cause.
Q2What would you alert on for a web application?
I page on symptoms users feel: the site or API failing external uptime checks, a sustained error rate above a threshold, latency far above normal, a critical flow such as checkout or login failing, and a TLS certificate close to expiry. I also alert, at lower priority, on things that will become outages if ignored: disk filling up, a queue backlog growing, failed or missing backup jobs. CPU spikes alone go to dashboards. Every alert needs a duration window so it does not flap, routing to the right person, and a runbook link. If an alert fires and nobody needs to do anything, I delete or tune it.
Q3Explain RPO and RTO.
RPO, recovery point objective, is the maximum amount of data loss the business accepts, expressed as time. If backups run nightly, you might lose up to a day of orders, so RPO is 24 hours. Continuous log shipping or replicas reduce it to minutes. RTO, recovery time objective, is the maximum acceptable downtime while you recover. A restore that takes six hours cannot meet a one-hour RTO. Both are business decisions with cost attached: lower numbers need replicas, point-in-time recovery and automation. The engineer's job is to make the trade-off clear, implement it and prove it with timed restore drills.
Q4Are server snapshots enough as a backup?
No, they are a useful layer but not a complete strategy. A snapshot usually lives with the same provider and often the same account, so a compromised account, billing problem or provider incident can take both the server and its snapshots. A snapshot of a running database may not be consistent unless the database is designed for crash recovery and you verify it. Snapshots also make it hard to restore one table or one customer's data. I combine them with logical database dumps and file backups, encrypted and copied off-site to separate storage, with retention rules and regular restore tests.
Q5What is the 3-2-1 backup rule?
Keep at least three copies of your data: the production copy and two backups. Store them on two different kinds of storage, for example the server disk and object storage, so one type of failure cannot destroy everything. Keep one copy off-site, in a different provider, region or account. A newer variant, 3-2-1-1-0, adds one immutable or offline copy that cannot be deleted even with admin credentials, which protects against ransomware, and zero errors, meaning backups are verified by actual restores. For a typical web app that means database dumps plus uploaded files, encrypted, with versioning or object lock on the remote bucket.
Q6How do you know your backups actually work?
By restoring them regularly, not by checking that files exist. I monitor that each backup job finished and produced a file of plausible size, and alert if it did not run. Then, on a schedule, I restore the latest backup into an isolated database or a temporary server, run sanity checks such as row counts and the newest timestamps on key tables, open a few uploaded files, and record how long the whole thing took. That timing is compared with the RTO. The steps live in a runbook, so the restore does not depend on one person remembering commands during an incident.
Senior-level questions
S1How would you define SLOs and alerting for a new service?
Start from user journeys, not from infrastructure. For an online shop that might be browsing, adding to cart and paying. For each, pick SLIs that reflect user experience, usually availability (non-5xx responses) and latency (the share of requests under a threshold), measured as close to the user as practical, at the load balancer or with synthetic checks. Agree targets with the business based on what users need and what today's system achieves; 99.9% is common, and every extra nine costs significantly more. Then alert on error-budget burn rate with multiwindow rules: fast burn pages, slow burn opens a ticket. Publish the budget, and agree a policy for when it is exhausted, such as pausing risky releases. Revisit the targets after a quarter of real data.
S2Pull-based or push-based metrics: what are the trade-offs?
In the pull model, used by Prometheus, the server scrapes each target on a schedule. That gives a free health signal (a failed scrape means the target is down or unreachable), central control of the scrape rate, and easy debugging, since you can open /metrics in a browser. It needs service discovery and network access from the monitoring server to every target, which is awkward across firewalls or for short-lived jobs. In the push model, used by StatsD, OpenTelemetry's OTLP exporters and many hosted vendors, clients send data to a collector. It works well for serverless functions, batch jobs and edge devices, but a silent client is indistinguishable from an idle one, and a misbehaving client can flood the backend. Many setups mix both: scrape long-running services and push from jobs through a collector or Pushgateway.
S3Replication, snapshots, backups: why are replicas not a backup?
A replica copies every change to the primary within seconds, which is exactly the problem: a DELETE without a WHERE clause, a bad migration or ransomware encryption replicates just as faithfully as good data. Replicas protect against hardware failure and support fast failover, improving RTO, but they do not let you go back in time. A backup is a separate, historical copy you can restore to an earlier point, ideally with PITR, stored independently and protected from deletion. Snapshots sit in between: point-in-time, but usually in the same account and provider. A sound design uses all three for different failures: replicas for availability, snapshots for fast whole-server recovery, and off-site, immutable logical or physical backups for data loss, verified by restore drills.
S4Log volume and cost have exploded. How do you bring them under control?
First measure: which services and which log lines produce the most bytes. Usually a few noisy sources dominate, such as debug logging left on, health-check access logs or stack traces repeated on every retry. Fix those at the source with correct levels and rate-limited logging. Then tier the data: keep full-detail logs for a few days in fast storage, aggregate or sample high-volume, low-value logs such as successful requests, and archive what compliance requires to cheap object storage. Turn repeated questions answered by grepping logs into metrics, which are far cheaper. In Loki, keep labels low-cardinality and filter by content at query time. The trade-off is investigative power: aggressive sampling can hide the one request you need, so always keep errors and slow requests unsampled.
Glossary
- SLI
- Service level indicator: a measured ratio of good events to total events, such as fast successful requests.
- SLO
- Service level objective: the target for an SLI over a time window, for example 99.9% over 30 days.
- Error budget
- The amount of failure an SLO allows; when it is spent, reliability work takes priority over features.
- Burn rate
- How fast the error budget is being used compared with spending it evenly across the whole window.
- Cardinality
- The number of unique label combinations, and therefore time series, a metric produces.
- Exporter
- A small program that reads stats from a system, like a database or host, and exposes them for Prometheus.
- Recording rule
- A PromQL expression Prometheus evaluates on a schedule and stores as a new series, making dashboards fast.
- WAL
- Write-ahead log: the database's sequential record of every change, used for crash recovery, replication and PITR.
- PITR
- Point-in-time recovery: restoring a database to any chosen moment by replaying logs over a base backup.
- Dead man's switch
- A monitor that alerts when an expected signal stops arriving, such as a backup job's success ping.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.