GitLab Database Deletion (2017)
Open in the interactive app: practice, quiz, mock interview →In one minute
- On 31 January 2017, while fighting a spike in database load and a broken replica, a GitLab.com engineer ran a command to wipe the replica's data directory on the primary database host by mistake. About 300 GB of production data was deleted before the command was stopped (GitLab postmortem).
- GitLab had five backup and replication mechanisms. When they needed one, none worked as expected: daily
pg_dumpbackups had been failing silently, disk snapshots were not enabled for the database servers, and replication had just been wiped. - They restored from a manual LVM snapshot taken about six hours earlier for staging. GitLab.com was down for about 18 hours, and data created in that window was lost (at least 5,000 projects, 5,000 comments and about 700 user accounts by GitLab's estimate; GitLab postmortem).
- GitLab's own root cause: no one owned the backup procedures, so they were never tested.
- They handled it with radical transparency: a public incident document, a live stream of the recovery and a detailed postmortem.
How it works
Timeline (UTC, from GitLab's postmortem)
- 31 Jan, ~17:20 An engineer takes a manual LVM snapshot of production to load into staging. This snapshot later saves the day.
- ~19:00 Database load rises, thought to be spam; many users cannot post comments. Bringing load under control takes hours. GitLab later found that an employee account-removal job, triggered by abuse reports, also contributed.
- ~23:00 Replication to the secondary falls behind and breaks, because the primary has already removed write-ahead log segments the secondary needs, and there is no WAL archiving. Resyncing with
pg_basebackuphangs; configuration changes and restarts follow (GitLab postmortem). - ~23:30 Trying to clear the secondary's data directory for a fresh resync, an engineer runs the removal on the primary. It is stopped within seconds, but about 300 GB is gone.
- The search for backups: the daily
pg_dumpto S3 has produced nothing: the S3 bucket is empty (GitLab postmortem). Azure disk snapshots are not enabled for database servers. The regular LVM snapshot is about 24 hours old. The manual one from 17:20 is about 6 hours old. - 1 Feb, ~17:00 After copying data from slow staging disks for about 18 hours, the database is restored from the 6-hour-old snapshot; webhooks and verification follow around 18:00.
Technical root cause
The trigger was a destructive command run on the wrong host during a stressful, late-night recovery. The reason it caused data loss rather than an annoyance was that the safety nets had holes. The pg_dump job ran on an application server using PostgreSQL 9.2 tools against a 9.6 database, so it failed (PostgreSQL docs: pg_dump refuses newer servers); the failure emails were rejected because DMARC was not set up for cron emails, so nobody knew. Disk snapshots covered NFS servers, not databases. LVM snapshots existed for staging, not for disaster recovery. Replication is not a backup: it faithfully copies deletions, and here it had been wiped as part of the repair (Google SRE book, Data Integrity).
Contributing factors
- No owner for backups. GitLab's 5 whys ends here: with nobody responsible, nobody tested restores.
- No WAL archiving, so the replica could not catch up after falling behind.
- Hosts that looked alike. Two database hosts with similar names and shells, in the middle of the night.
- Load from abuse handling. An account-removal process that was too easy to trigger added load.
- Slow restore path. Staging used slow disks in another region, so copying data took most of the 18 hours.
Detection and mitigation
The deletion was noticed immediately by the engineer. The real "detection" problem was earlier and silent: backups had been failing for some time with no working alert. Mitigation was the restore itself, chosen as the least-bad option among broken backups.
What changed afterwards
GitLab's action items included hourly LVM snapshots and Azure disk snapshots for database servers, automated testing of backup restores, a backup monitoring dashboard, point-in-time recovery with continuous WAL archiving, better replication runbooks, sensible max_connections, an owner for data durability, and a redesign of abuse reporting so account removals are not triggered so easily.
What I would have done as the tech lead
Before: name an owner for data durability and require a scheduled, automated restore test with alerting on the result, not on the backup job's exit status. Enable WAL archiving and point-in-time recovery. Make destructive commands on database hosts require typing the host name, and colour-code production prompts. During: stop and hand off when the person doing recovery has been fighting the incident for hours; destructive steps in a recovery should be read aloud and confirmed by a second person. After: GitLab's transparency is worth copying, with names redacted, as they later decided.
Down the stack
Layer 1The contract
GitLab.com's implied contract was that user data was durably stored and recoverable. Their backup setup looked redundant on paper, five mechanisms, but none of them had a verified contract like "we can restore the production database to within 1 hour of any point in the last 7 days, in under 2 hours". Without a tested restore, a backup is an untested hypothesis.
Layer 2The mechanism
PostgreSQL streaming replication sends write-ahead log records from the primary to replicas. The primary keeps only a limited amount of WAL (controlled at the time by wal_keep_segments); if a replica falls further behind than that and there is no archive to fetch older segments from, it cannot catch up and must be rebuilt from a fresh base backup with pg_basebackup, which copies the whole data directory. pg_dump produces a logical backup and must be run by a version of the tools at least as new as the server, which is exactly what failed here. Point-in-time recovery needs a base backup plus continuous WAL archiving, which GitLab did not yet have (PostgreSQL docs).
Layer 3The machine
Recursively removing a data directory unlinks the files; for a database whose files are open, the space may not be freed until the process closes them, but the directory entries are gone and recovery from the file system is not practical. The 18-hour restore was dominated by copying hundreds of gigabytes from slow network-attached staging disks in another region, a throughput problem: at around 5 to 10 MB/s, 300 GB takes 8 to 17 hours. Fast restores need fast storage and a nearby copy, which is why restore time must be measured, not assumed (Google SRE book, Data Integrity).
Why it's built this way
- Backups exist to be restored, so only restore tests prove they work.
- Silent failure is the default for unattended jobs, so success must be actively verified.
- Replication protects against machine loss, not against deletions, so it is not a backup.
- Stressful late-night recoveries make mistakes likely, so destructive steps need friction and a second person.
- Ownership drives testing; without an owner, nothing gets verified.
Diagram
flowchart TD
L["Load spike: spam and account removals"] --> R["Replica falls behind, no WAL archive"]
R --> RB["Rebuild replica, pg_basebackup hangs"]
RB --> W["rm -rf run on primary by mistake"]
W --> D["~300 GB deleted"]
D --> B{"Backups?"}
B --> B1["pg_dump: silently failing for days"]
B --> B2["Azure snapshots: not on DB servers"]
B --> B3["Replica: wiped"]
B --> B4["LVM snapshot: 6 h old, manual"]
B4 --> RS["18 h restore from slow staging disks"]Worked example
Apply it to your own team. You have nightly pg_dump backups to object storage, a streaming replica, and daily volume snapshots. List each one as GitLab should have: what it protects against, last success, last restore test, alert on failure, and restore time.
The audit finds: pg_dump succeeds but has never been restored; the dump of the 600 GB database takes 5 hours to restore. The replica protects against host loss only. Volume snapshots succeed, but the last restore test was 14 months ago. Alerts for backup failure go to an email list nobody reads.
Changes: WAL archiving with point-in-time recovery (RPO about 1 minute); a weekly automated restore of the latest base backup plus WAL to a scratch instance, running row-count checks and alerting on failure through the paging system; a named owner on the database team; and a host prompt that shows PROD-PRIMARY in red. The first automated restore test fails, because the archive bucket's lifecycle policy deleted WAL older than 3 days, shorter than the 7-day base backup interval. Exactly the kind of gap that would only have shown up on the worst day.
In the wild
- Atlassian, 2022: backups that could not be restored at scale. After a deletion script removed 883 sites, 30-day backups and point-in-time recovery existed for each data store, but there was no automated way to restore many sites with all their products into the live environment without affecting others. Restoration took up to 14 days for some of the 775 customers (Atlassian post-incident review).
- Google Music, 2012: a restore from tape took nearly a week. Google's SRE book describes restoring 1.5 petabytes from offsite tape, which took nearly seven days. It contrasts this with the 2011 Gmail restore, which finished within hours of the estimate because the team had rehearsed it (Google SRE book, Data Integrity).
Trade-offs
| Mechanism | Protects against | Does not protect against | Restore speed |
|---|---|---|---|
| Streaming replica | Host failure | Deletions, corruption (copied instantly) | Seconds (failover) |
Logical dump (pg_dump) | Deletion, corruption | Data since last dump | Slow for large databases |
| Disk or volume snapshots | Deletion, corruption | Data since last snapshot | Medium |
| Base backup plus WAL archive (PITR) | Deletion, corruption, to any point | Loss of the archive itself | Medium, depends on replay |
| Restore tests | Undetected broken backups | Nothing by themselves; they prove the others work | n/a |
Pitfalls in production
- Monitoring backup jobs' exit codes instead of restoring. Fix: automated restore tests with verification.
- Treating replication as a backup. Fix: separate, point-in-time-capable backups.
- Alerts delivered to channels nobody reads, or not delivered at all. Fix: route backup alerts through the paging system and test them.
- Look-alike production hosts. Fix: distinct, coloured prompts and confirmation for destructive commands.
- Exhausted engineers performing destructive recovery steps. Fix: hand-offs and two-person confirmation.
Leadership lens
- Assign an owner for data durability and measure them on restore tests, not backup jobs.
- Make restore time a tracked number for each critical data store.
- Treat destructive commands in production as gatekeeper-zone work: friction, confirmation, a second person.
- Learn from GitLab's transparency: honest public communication rebuilt trust faster than silence would have.
Check yourself
Why did GitLab's replica not help recover the deleted data?
Show answer
Replication protects against losing a machine, not against data loss on the primary; in this case the replica had also been cleared as part of the rebuild.
Why did the
pg_dumpbackups fail silently?Show answer
They ran with PostgreSQL 9.2 tools against a 9.6 server and failed, and the failure emails were rejected because DMARC was not configured, so nobody was alerted.
What single organisational gap did GitLab identify as the root cause of the backup failures?
Show answer
Nobody owned backups, so nobody was responsible for testing that restores worked.
Why did the restore take about 18 hours?
Show answer
The usable snapshot was on slow staging storage in another region, so copying hundreds of gigabytes took most of that time.
Sources and further study
Primary
- Postmortem of database outage of January 31 (GitLab): timeline, failed backups and action items.
Systems docs
- pg_dump, PostgreSQL 9.6 docs: the version rule that made the nightly dump fail.
- Continuous archiving and point-in-time recovery (PostgreSQL): base backup plus an unbroken WAL archive, set up and tested first.
Books
- Site Reliability Engineering (Google), Ch 26: Data Integrity, on layered defences (soft deletion, tiered backups, early detection) and why restores are the goal.
- Release It!, 2nd ed., Michael Nygard, Ch 3: Stabilize Your System, on failure modes and stopping crack propagation.
Other curricula
- Postmortem Culture, SRE Workbook Ch 10 (Google): blameless reviews that improve the system, which GitLab's public document followed.
Free resources
- Postmortem of database outage of January 31 (GitLab): GitLab's own detailed postmortem, timeline and action items.
- Continuous archiving and point-in-time recovery (PostgreSQL): the capability GitLab lacked.
- pg_basebackup (PostgreSQL): taking base backups for replicas and recovery.
- Data Integrity (Google SRE book): why restores, not backups, are what matter.