Proving you can restore  / field guide
Practitioner field guide · 2026-09-11

A backup is a claim. A restore is the proof.

How production teams prove they can get their data back, reconstructed from the public operational record: GitLab's postmortems, runbooks, policies and restore pipelines from 2017 to 2026, Google's account of deleting a customer's private cloud, and the design documents of the backup tools themselves.

25 primary sources 8 organisations 6 published incidents Evidence through Sep 2026 Read: 22 min
01

The territory

The problem, stated without naming a product: when the data a system runs on is destroyed, does a second copy exist that can actually be brought back, completely, within a bounded time, from a failure domain the disaster does not share, and how do you know that before the day it matters?

5
Backup and replication mechanisms GitLab had on 2017-01-31. Working restores produced: none.
18 h
Time to copy the one usable snapshot back to production, over disks throttled to roughly 60 Mbps.
~9 h
Daily WAL-G full backup of the ~14 TiB GitLab.com database, as of September 2021.
S4
Severity when the 2017 failure class returned in July 2026 and the restore pipeline's dead-man switch caught it. Customer impact: none.

On 31 January 2017 an engineer deleted roughly 300 GB from GitLab.com's primary database. The postmortem's sentence about what happened next is the founding document of this whole topic: "the process of both finding and using backups failed completely." The pg_dump job had been failing for months because the tool was version 9.2 and the database was 9.6, so the S3 bucket was empty. The failure emails had been silently rejected for want of a DMARC signature. Azure disk snapshots were not enabled for the database hosts. Replication was the thing that had just broken. The recovery ran from a staging LVM snapshot an engineer happened to have taken six hours earlier, copied back over disks that a cost decision had throttled to about 60 Mbps, for 18 hours.

On 20 July 2026, the same organisation hit the same failure class again: after an upgrade to PostgreSQL 17, the restore pipeline was still configured for PostgreSQL 16. The difference is the whole subject of this guide. In 2017 the version mismatch was discovered at 23:00 on the worst night of the company's life, because the only thing that ever exercised the backups was a disaster. In 2026 it was discovered by a Dead Man's Snitch on a CI pipeline that restores the production database every day, filed at severity 4, and fixed by changing two variables. Nine years of work did not eliminate the failure class. It demoted it.

Figure 1 · What "we have backups" actually claims

2017: the S3 bucket was empty

2026: pipeline still configured for pg16

2017: 18 h at 60 Mbps

etcd: clients see revisions go backwards

Backup job ran green

Artifact exists where the runbook says

Restore tooling matches today's production

Restore completes inside the RTO

The system behaves after the restore

2017: the S3 bucket was empty

2026: pipeline still configured for pg16

2017: 18 h at 60 Mbps

etcd: clients see revisions go backwards

Backup job ran green

Artifact exists where the runbook says

Restore tooling matches today's production

Restore completes inside the RTO

The system behaves after the restore

Every link in this chain has a documented break in the evidence below; a green backup job asserts only the first box. Sources: GitLab 2017, GitLab 2026, etcd docs.
Diagram source

This guide reconstructs how teams that have been burned close that chain: what the verification layer looks like, which decisions shape it, what has broken in production, and the numbers a design review can plan against. It is built almost entirely from repository records: postmortems and incident issues, runbooks, controlled policy documents, merge requests and tool design docs, because that is where this practice actually lives, in public, with dates on it.

Evidence base, honestly

This dig was researched from a network position that could reach git hosting (gitlab.com, github.com) and cloud.google.com but not engineering-blog or conference hosts. Atlassian's 2022 post-incident review, Roblox's 2021 postmortem, Cloudflare's November 2023 disaster-recovery account and the USENIX FAST backup-systems literature all exist and are relevant; they were unreachable this session, are not cited, and no claim below leans on them. The corpus is therefore deep on GitLab, which operates its production infrastructure in the open, plus Google's GCVE incident report and five tools' own records. Treat the GitLab material as one organisation's account, corroborated where the tool documentation independently agrees.

Deliberately out of scope: ransomware-specific immutability products, tape and media management, legal-deletion pipelines, application-level export formats, and multi-region active-active designs where nothing is ever restored because everything is always live. Those are adjacent problems with their own records.

02

How it is actually built

The common shape of a backup system that can prove itself, assembled from GitLab's policies and runbooks, Google's incident report, and the design documents of restic, Velero, PostgreSQL and etcd. Every box is attributable.

Figure 2 · Reference architecture: the verification layer is the product

Verification layer, runs as CI

Storage outside the failure domain

Production failure domain

WAL, continuous

success ping

success ping

drills feed

Primary
datastore

Disk snapshots
hourly

Multi-region
object storage

Deletion firebreak
versioning + soft delete

Daily PITR restore
into a fresh instance

Hourly restore of a
random sampled snapshot

Dead-man snitch
alerts on silence

Quarterly gameday
RTO measured and logged

Verification layer, runs as CI

Storage outside the failure domain

Production failure domain

WAL, continuous

success ping

success ping

drills feed

Primary
datastore

Disk snapshots
hourly

Multi-region
object storage

Deletion firebreak
versioning + soft delete

Daily PITR restore
into a fresh instance

Hourly restore of a
random sampled snapshot

Dead-man snitch
alerts on silence

Quarterly gameday
RTO measured and logged

The left half is what most teams build; the right half is what distinguishes the teams with a public record of surviving. Reconstructed from GitLab's DR policy, its runbooks and Google's GCVE report.
Diagram source

Strip away the products and four structural commitments recur.

Two backup kinds per datastore, because they fail differently. GitLab's current policy pairs a continuous WAL archive with hourly disk snapshots for the same database, and hourly block snapshots for Git data. The PostgreSQL documentation is blunt about why one kind is not enough: file-system-level backups "only work for complete backup and restoration of an entire database cluster", while WAL archiving gives point-in-time precision but "must be used with caution: if WAL archiving is not monitored correctly", the chain silently loses its value. The 2017 outage is the degenerate case: five mechanisms that were really one mechanism, because none had an independent, exercised restore path.

The copy lives outside the failure domain, and the failure domain includes your account. The 2024 Google Cloud incident is the sharpest published lesson here. A blank parameter in an internal provisioning tool gave UniSuper's private cloud "a then unknown default fixed 1 year term value", and at the end of the term the platform deleted the deployment. Google's own report notes that backups stored in Google Cloud Storage survived, and that the customer's "third party backup software" was "instrumental in aiding the rapid restoration". The transferable reading: deletion arrived through the provider's own control plane, so the copy that mattered most was the one a different organisation held. GitLab's policy encodes the same idea one level down, with GCS soft delete at 7 days and object versioning as a firebreak between "deleted" and "gone".

A machine restores on a schedule and a human is paged only when it goes quiet. This is the part most teams do not build. GitLab's controlled DR policy commits to a daily CI pipeline that "performs a point-in-time recovery (PITR) restore into a new instance and verifies data integrity by running queries", and to hourly verification that takes "a randomly selected Gitaly disk", restores it to a new disk, and checks for recent commits. The design dates to 1 February 2017, the day after the outage, in issue #1102: the restore test "should use the same underlying tools as a production engineer might use", exercised automatically, "removing the need for engineers to do this manually". The alerting on top is deliberately inverted: the pipeline pings a dead-man snitch on success, and the alert fires on silence. The 2017 outage had alert-on-failure, and the failure emails died in DMARC.

The restore path is code, and it rots like code. The 2026 incident record shows what the verification layer actually catches in a steady state: a PostgreSQL major upgrade the pipeline had not followed (July 2026), missing scheduled GCP snapshots (June 2026, March 2026), snitch gaps after refactors (May and June 2026). A closed merge request from April 2026 shows the same rot arriving from below: a Red Hat security patch to GNU tar made GitLab's restore invocation fail on EL9 systems, because the restore code passed flags whose behaviour the patch changed. None of these were data loss. All of them would have been, on the day, if the first time the path ran was the disaster.

The pipeline is organised per datastore

GitLab's restore practice is a group of CI projects, one per thing that must come back: the main database, the subscriptions database, Gitaly disks, Ops and Staging snapshots. Each is a pipeline whose job is a restore.

Recorded at: gitlab-restore group, since 2019

Verification has a cost model

restic's documentation warns that a full data check "has to download all pack files in the repository", with the bandwidth bill that implies, and documents random and n-of-t sampling as the affordable alternative. GitLab samples Gitaly disks hourly rather than restoring the fleet.

Recorded at: restic docs, GitLab backup policy

Drills carry a confidence taxonomy

GitLab's gameday runbook defines four confidence levels per service, and the top one is unreachable by analysis: "High confidence" requires "We have tested recovery in production". Mock DR events run almost weekly; each drill's wall-clock numbers are committed to the runbook.

Recorded at: gameday.md, recovery-measurements.md

"One thing is having snapshots, a different thing is having a backup that we can recover from." GitLab infrastructure meta-issue #1161, "Disaster recovery for everything that is not the database", 10 February 2017
03

The decisions that matter

Each of these is a fork with a published rationale on at least one side, and a condition that flips it.

Do you verify the artifact, or verify the restore?

Chosen
  • GitLab restores: a daily PITR into a fresh instance with integrity queries, and an hourly random snapshot restore checking for recent commits. The test is the production procedure, per issue #1102's design.
Rejected as sufficient
  • Checksums and structural checks of the stored artifact. restic's default check validates structure; reading the data is a separate, costed flag. A checksum proves the bytes; it says nothing about version drift, tooling, credentials or wall-clock time.
Flips when
  • Never fully, but the restore cadence flips with blast radius: full scheduled restores for the datastore whose loss is the company (daily), artifact-level sampling for bulk fleets (hourly, random), documented exemption where the store itself provides versioned undelete.

Alert when the backup fails, or when the verifier goes quiet?

Chosen
  • Alert on silence. GitLab's restore pipelines ping a Dead Man's Snitch on success; the July 2026 pg16/pg17 drift and the May 2026 reporting gap both surfaced as "snitch missing" incidents, at severity 4.
Rejected
  • Alert on failure. 2017's cron jobs did notify on error, by email, which DMARC rejected. A failure alert shares fate with the failing system; the postmortem records that nobody knew the S3 bucket had been empty for months.
Flips when
  • It does not. Alert-on-failure can coexist, but the silence alarm is the one that survives the failure modes you did not enumerate, because its trigger is the absence of proof rather than the presence of an error.

Logical dumps or physical, WAL-based backups?

Chosen
  • Physical base backups plus continuous WAL, at any real size. GitLab's runbook: the daily WAL-G base backup of a ~14 TiB cluster takes ~9 hours, and WAL-G restores the basebackup phase 3-4x faster than its predecessor.
Rejected
  • pg_dump as the disaster mechanism. It was GitLab's headline backup in 2017, version-fragile and unmonitored. A 2026 closed merge request records the current gap for self-managed installs: above roughly 100 GB the official guidance stops offering a practical bundled answer.
Flips when
  • Below tens of gigabytes, logical dumps win on simplicity, portability across versions, and partial-restore ergonomics. The PostgreSQL docs' constraint decides it: physical backups are all-or-nothing for the whole cluster.

Figure 3 · How much verification does this datastore need?

yes

no

yes

no

yes

no

Is loss of this store
loss of the product?

Does the store provide
versioning and soft delete?

Can you afford to read
every byte back?

Scheduled full restore into a
fresh instance, integrity queries,
dead-man alerting. Daily.

Document the exemption in a
controlled policy, with the reason.
Review when the threat model moves.

Full read-back verification
on every cycle

Random-sample restores
on a schedule. Hourly beats
thorough-but-never.

yes

no

yes

no

yes

no

Is loss of this store
loss of the product?

Does the store provide
versioning and soft delete?

Can you afford to read
every byte back?

Scheduled full restore into a
fresh instance, integrity queries,
dead-man alerting. Daily.

Document the exemption in a
controlled policy, with the reason.
Review when the threat model moves.

Full read-back verification
on every cycle

Random-sample restores
on a schedule. Hourly beats
thorough-but-never.

A decision tree assembled from GitLab's policy exemption for object storage, restic's sampling flags, and the daily-restore commitment for the primary database. Terminal nodes are actions with cadences, not sentiments.
Diagram source
DecisionChosenRejectedBecauseFlips whenEvidence
Proof of restorabilityScheduled real restoresArtifact checksums aloneRestores exercise tooling, versions, credentials and time, not just bytesCadence and depth scale down with blast radiusGitLab DR policy
Verifier alertingDead-man switch on success pingsEmail on failureThe failure channel failed silently in 2017 (DMARC)Does not flipIncident #22543
Verification depthRandom sampling (hourly disk, n-of-t packs)Full read-back every cycleFull read-back downloads the whole repository; bandwidth and timeSmall repositories, or after any corruption sightingrestic docs
Database backup kindPhysical base + WAL (PITR)pg_dump as the disaster pathSpeed and point-in-time precision at multi-TiB scaleBelow tens of GB, logical wins on portabilityWAL-G runbook, MR !229743
Where the last copy livesOutside the account and provider scopeAll copies under one control planeThe GCVE deletion arrived through the provider's own toolingNever fully; cost decides how much lives outsideGoogle, 2024
Object storage restore testsExempt, in writingTesting everything uniformlyVersioning plus soft delete cover the deletion vector the tests would catchThe store's own account or region is the thing at riskGitLab backup policy

The exemption row deserves a sentence, because it is the only place the two best sources in this corpus disagree in spirit. GitLab exempts object storage from restore testing on the strength of versioning and soft delete. Google's GCVE report shows a deletion vector that ran above those protections: the subscription itself was deleted by the platform. Both are right in their own threat model. The reader's job is to say, in writing, which threat model their exemption assumes; the flip condition is the moment that assumption stops holding.

04

What broke in production

Six published incidents, grouped into the four classes this corpus keeps producing: drift severs the restore path, the notification channel fails silently, the backup shares the primary's failure domain, and the backup process is itself production load.

Figure 4 · The night of 31 January 2017, as a protocol

Staging LVMsnapshotReplicaAzure disk snapshotspg_dump backups onS3On-call engineersStaging LVMsnapshotReplicaAzure disk snapshotspg_dump backups onS3On-call engineersWebhooks recovered separately from a second snapshot copyfetch the latest dumpbucket is empty, job failed formonths on 9.2 vs 9.6restore a disk snapshotnever enabled for the database hostspromote the secondaryreplication broken, data already wipedcopy a 6-hour-old snapshot taken by hand18 hours at about 60 Mbps
Staging LVMsnapshotReplicaAzure disk snapshotspg_dump backups onS3On-call engineersStaging LVMsnapshotReplicaAzure disk snapshotspg_dump backups onS3On-call engineersWebhooks recovered separately from a second snapshot copyfetch the latest dumpbucket is empty, job failed formonths on 9.2 vs 9.6restore a disk snapshotnever enabled for the database hostspromote the secondaryreplication broken, data already wipedcopy a 6-hour-old snapshot taken by hand18 hours at about 60 Mbps
On the day, you restore from the intersection of what still works, at the speed of the cheapest disk in the chain. Sequence reconstructed from GitLab's postmortem.
Diagram source
Postmortem

Five mechanisms, zero restores

AssumptionFive overlapping backup and replication mechanisms meant at least one working recovery path.
What happenedpg_dump silently broken by a version mismatch, its S3 bucket empty; Azure snapshots not enabled for database hosts; LVM snapshots informal; replication was the broken thing; failure email eaten by DMARC.
Blast radius~6 hours of database writes lost (about 5,000 projects, 5,000 comments, 700 accounts); many hours of downtime; 18-hour copy to restore.
FixAutomated restore testing designed the next day (issue #1102), backup monitoring by Prometheus, WAL-based backups, hourly snapshots, ownership for data durability.
Design ruleCount restore paths you have exercised this month; every other number is decoration.
Postmortem

Broken again, eleven days later

AssumptionAfter the most public backup failure in the industry's memory, the rebuilt pipeline was being watched closely enough.
What happenedOn 11 February 2017, one day after the postmortem published, an engineer filed "Backup system and replication is not working": the secondary could not stream because the needed WAL segment "has already been removed", and, in the issue's words, "our backups are simply not working".
Blast radiusNo data loss; a window with degraded redundancy while rebuilt by hand.
FixThe WAL-shipping design (WAL-E, then WAL-G) that decoupled backup from replication-slot retention; later, daily automated restore verification.
Design ruleA fix made under postmortem pressure is a prototype. Until a machine exercises it on a schedule, you have a newer unverified backup system, not a verified one.
Postmortem

The backup caused the failover

AssumptionCloud disk snapshots are "a transparent background transaction via GCP API" with no effect on the running node, in the incident's own words.
What happenedSnapshots were initiated on all database servers concurrently; within four minutes error rates spiked, Patroni invalidated the primary's lock and failed over.
Blast radiusProduction database failover and service disruption, January 2021.
FixSnapshot scheduling that respects the cluster: staggered, replica-preferring, treated as load.
Design ruleThe backup process is a production workload with a capacity cost. GitLab's runbook prices it: backup-push is "very IO-intensive" at 0.5-1 TiB/h. Budget it like any other consumer.
Postmortem

The verifier goes dark, and that is the alert

AssumptionA PostgreSQL major upgrade is done when production is upgraded.
What happenedAfter the pg17 upgrade, the daily restore pipeline kept looking for pg16 backups; its success ping stopped; the Dead Man's Snitch fired. 2017's failure signature, version drift between backup tooling and database, resurfacing in the safest possible place.
Blast radiusSeverity 4. "There does not appear to be any customer impact." Fixed by updating two pipeline variables.
FixPipeline reconfigured; the incident record itself is the audit trail.
Design ruleYou do not eliminate drift; you choose where it is discovered. A daily restore moves the discovery from your worst day to an ordinary Tuesday.
Postmortem

The provider deleted the cloud, term unknown to everyone

AssumptionDeployment across two geographies protects against loss; the platform will not delete a live, paid-for private cloud.
What happenedDuring provisioning of UniSuper's GCVE private cloud in 2023, "one input parameter was left blank", so an internal tool assigned "a then unknown default fixed 1 year term". At term end in May 2024 the platform deleted the deployment.
Blast radiusOne customer's private cloud; restoration took teams working "24x7 over several days". GCS backups in-region survived; third-party backups were "instrumental in aiding the rapid restoration".
FixGoogle deprecated the tool, removed the manual parameter, scrubbed the database for other time-bombed deployments, and corrected the deletion behaviour.
Design ruleReplication multiplies copies inside one administrative scope; a deletion that arrives through that scope takes them all. Keep one copy whose custodian cannot be reached by your provider's control plane.
Source

The OS patch that broke restore, not backup

AssumptionA restore path that worked at release keeps working on the distributions customers run.
What happenedA 2026 Red Hat security update changed GNU tar so that the unlink flags GitLab's restore passed made extraction fail on EL9. Backups were fine; restoring them was not. The fixing merge request notes the flags were redundant all along.
Blast radiusSelf-managed restores on patched EL9+ hosts, until fixed.
FixRemove the flags; rely on the existing pre-restore file move.
Design ruleRestore code has dependencies that upgrade underneath it. If your restore test matrix does not include the OS your operators actually patch, your environment drift is untested drift.

A fourth class hides behind all of these: restoring the data is not restoring the system. etcd's documentation is the cleanest statement in the corpus. After a snapshot restore, "existing clients may perceive the revision going back", and for Kubernetes specifically, "restoring to an older revision may not correctly refresh the caches, causing unpredictable and inconsistent behavior in the controllers". GitLab met the same class in 2017: the recovered snapshot had webhooks deliberately stripped (it was a staging copy), so webhooks were surgically re-imported from a second snapshot, and every database sequence was advanced by 100,000 so restored IDs could not collide with IDs handed out before the outage. A restore plan that ends at "data directory present" has not planned the part where the system re-enters a world that moved on.

Figure 5 · One failure class, nine years, demoted

2017pg_dump broken by9.2 vs 9.6six hours of data lostrestore testingdesigned in issue11022019gitlab-restore CI groupcreated2021failing verification is adeclared incident2024gameday restorestimed and logged2026pg16 to pg17 driftsnitch firesseverity 4, no impactVersion drift, 2017 to 2026
2017pg_dump broken by9.2 vs 9.6six hours of data lostrestore testingdesigned in issue11022019gitlab-restore CI groupcreated2021failing verification is adeclared incident2024gameday restorestimed and logged2026pg16 to pg17 driftsnitch firesseverity 4, no impactVersion drift, 2017 to 2026
Version drift between backup tooling and database never went away; the machinery around it changed what it costs. Dates from the incident record and issue tracker.
Diagram source
05

Numbers you can plan against

Everything quantitative this corpus yields, dated and sourced. The pattern worth carrying: backup duration, restore duration and drill duration are three different numbers, and only the third one is measured with humans in the loop.

MetricValueAtContextAs ofSource
Data removed by the accident~300 GBGitLabrm -rf on the primary; ~4.5 GB remained2017-01postmortem
Data loss window~6 hGitLab~5,000 projects, ~5,000 comments, ~700 accounts2017-01postmortem
Restore copy time~18 hGitLabstaging disks throttled to ~60 Mbps, a cost choice2017-01postmortem
Cluster size~14 TiBGitLabmain GitLab.com PostgreSQL cluster2021-09runbook
Daily full backup duration~9 hGitLabWAL-G backup-push, run on a secondary2021-09runbook
Backup push throughput0.5-1 TiB/hGitLab"very IO-intensive"; a capacity line item2021runbook
WAL-G vs WAL-E restore3-4×GitLabbasebackup fetch phase, n1-standard-162020runbook
Backup cadence, databasehourly + WALGitLabfull backup every hour, continuous log archival2026policy
Retention / soft delete14 d / 7 dGitLabbackups 14 days; GCS soft delete 7 days2026policy
Restore test cadence, databasedailyGitLabPITR into a fresh instance, integrity queries2026DR policy
Restore test cadence, Git datahourlyGitLabrandom Gitaly disk, recent-commit check2026DR policy
Gameday: recreate 45 prod Gitaly VMs39 minGitLabproduction drill, plus ~11 min bootstrap2024-10measurements
Gameday: zonal DR process2 h 38 mGitLabstaging, end to end including MRs and checks2026-07measurements
Deletion timer nobody knew about1 yearGoogle GCVEblank parameter defaulted a fixed term2023-2024Google
GCVE recovery effortdays, 24x7Google / UniSuperthird-party backups called "instrumental"2024-05Google
Read these carefully

All figures above are operator-reported, not independently measured, and the GitLab throughput numbers date from 2021 against hardware that has since changed. Two quantities this corpus does not contain, because the reachable record does not publish them: the money cost of running a daily restore pipeline, and any cross-organisation distribution of restore times. Where a number was needed and none exists, this page says so rather than estimating.

06

The evidence wall

Every source behind this page, graded. All links were fetched and read on 2026-09-11. No papers and no talks appear: their hosts were unreachable from this session's network, which is stated rather than papered over.

Postmortem GitLab2017-02-10

Postmortem of database outage of January 31

The canonical backup-failure postmortem, read here from its markdown source in the website repository at a 2017 commit. Five mechanisms enumerated, each with the specific reason it produced nothing restorable.

Carry forwardOverlapping backup mechanisms are one mechanism until each has an independently exercised restore path.
gitlab.com/gitlab-com/www-gitlab-com @ 30d10754
Decision record GitLab2017-02-01

Issue #1102: Automated testing of recovering PostgreSQL database backups

Written the day after the outage: a weekly host restores the latest backup with the production tools, checks that users, projects and issues contain data, and alarms on failure. The design the next decade implemented and tightened to daily.

Carry forwardThe restore test must use the same tools an engineer would use at 3 a.m., or it verifies the wrong procedure.
gitlab-com/gl-infra #1102
Postmortem GitLab2017-02-11

Issue #1168: Backup system and replication is not working

Eleven days after the outage, one day after the postmortem published: the WAL segment the secondary needed was already gone, and "our backups are simply not working". The rawest artifact in this corpus.

Carry forwardPost-incident fixes fail at the same rate as other new systems. Verification, not resolve, is what changed the curve.
gitlab-com/gl-infra #1168
Decision record GitLab2017-02-10

Issue #1161: Disaster recovery for everything that is not the database

The widening of scope, twelve numbered workstreams, and the sentence quoted at the top of section 02 distinguishing having snapshots from having a recoverable backup.

Carry forwardInventory every datastore that is not the headline database; the postmortem's list had eight of them.
gitlab-com/gl-infra #1161
Source GitLabchecked 2026-09

Runbooks: disaster-recovery/README.md

The operator-facing map: backups tolerant of zonal and regional loss via multi-region object storage, with restore validation running "in CI pipelines for both the Postgresql database and disk snapshots".

Carry forwardThe runbook links the validation pipelines inline; recovery documentation that does not link its own proof is aspiration.
gitlab-com/runbooks
Source GitLabchecked 2026-09

Runbooks: gameday.md

Mock DR events "almost every week", explicit roles, and a four-level confidence taxonomy per service in which High confidence requires recovery tested in production, not in a production-like environment.

Carry forwardGrade every service on that four-level scale; the honesty of "No confidence: we have not tested recovery" is the point.
gitlab-com/runbooks
Case study GitLab2024-2026

Runbooks: recovery-measurements.md

A running log of drill timings: Gitaly VM recreation between 14 and 76 minutes across eight dated entries, a 2 h 38 m zonal DR process, notes on the drill where node boot-up needed debugging.

Carry forwardRTO is a measurement with a date and a worst-case column, not a target in a slide.
gitlab-com/runbooks
Source GitLab2021, checked 2026-09

Runbooks: PostgreSQL backups with WAL-G

The operational anatomy: daily base backup from a Consul-elected secondary, WAL shipped continuously to one bucket per environment, ~9 h for ~14 TiB, restore automated in the gitlab-restore CI project, delayed replica for fast PITR.

Carry forwardRun the base backup on a replica and treat its IO as budgeted load; 2021's #3338 is what unbudgeted looks like.
gitlab-com/runbooks
Policy GitLabchecked 2026-09

Controlled document: Backups of GitLab.com

The audited policy: hourly full database backups with continuous log archival, 14-day retention, GCS soft delete at 7 days, snapshot restore validation by random sampling, and object storage explicitly exempted with the reason written down.

Carry forwardExemptions from restore testing belong in a controlled document with their threat-model assumption attached.
handbook source
Policy GitLabchecked 2026-09

Controlled document: Disaster Recovery of GitLab.com

Daily PITR restore testing with data-integrity queries for the main and subscriptions databases; hourly random-disk Gitaly snapshot restores verified by recent commits; quarterly gamedays validating RTO and RPO against recorded measurements.

Carry forwardThree cadences, three depths: daily full restore, hourly sample, quarterly human drill. Copy the structure before the tools.
handbook source
Postmortem GitLab2021-03-24

Incident #4056: Our database backups are failing to verify

A declared production incident whose customer impact field reads "DB Backup Verification". The verification pipeline failing is treated with the same machinery as user-facing outages: declared, timelined, given corrective actions.

Carry forwardPage on a failing verifier with the same urgency as a failing service; it is the same risk on a delay.
gl-infra/production #4056
Postmortem GitLab2021-01-16

Incident #3338: database failover triggered by GCP snapshots

Concurrent snapshots on all database nodes, begun "under the mistaken impression that the snapshot process was a transparent background transaction", cost the primary its Patroni lock inside four minutes.

Carry forwardStagger snapshots, prefer replicas, and load test the backup path itself.
gl-infra/production #3338
Postmortem GitLab2026-07-20

Incident #22543: gitlab-restore/postgres-gprd snitch missing

The surprise at the top of this page: post-upgrade version drift, 2017's signature failure, caught by the restore pipeline's dead-man switch as a severity-4 item with no customer impact. Sibling incidents in May and June 2026 show the switch working repeatedly.

Carry forwardJudge a backup program by what severity its recurring failure classes land at, not by whether they recur.
gl-infra/production #22543
Source GitLab2026-04, closed unmerged

MR !231770: Remove --unlink-first and --recursive-unlink from tar extract

A distribution's security patch to GNU tar broke GitLab's restore invocation on EL9+. The change removes flags the restore never needed; closed without merge, the argument preserved.

Carry forwardRestores fail on environments, not just on artifacts; the test matrix has an OS dimension.
gitlab-org/gitlab !231770
Source GitLab2026-03, closed unmerged

MR !229743: Add Barman backup option for large database (draft)

A closed draft that names the gap in official guidance: bundled databases over roughly 100 GB have outgrown pg_dump-based backups, and the documented answer, cloud-managed databases, is unavailable to many self-managed customers.

Carry forward~100 GB is a documented flip point where logical dumps stop being a disaster mechanism.
gitlab-org/gitlab !229743
Documentation GitLabchecked 2026-09

Docs: Restore GitLab

The restore contract, stated flatly: "You can only restore a backup to exactly the same version and type (CE or EE)" of GitLab it was created on. The sentence that makes version drift a standing threat rather than an edge case.

Carry forwardRecord the full version matrix beside every backup artifact; the artifact alone is not restorable knowledge.
gitlab-org/gitlab docs
Source GitLabgroup since 2019-01

The gitlab-restore group

The practice as an org chart artifact: "Backup restore procedures for GitLab", with CI projects for the gprd database, the subscriptions database, Gitaly snapshot verification, and snapshot verification for Production, Staging and Ops.

Carry forwardOne restore pipeline per datastore, named, owned and visible; a shared script named backup.sh is none of those.
gitlab.com/gitlab-restore
Postmortem Google Cloud2024-05-24

Details of Google Cloud GCVE incident

Google's own account of deleting UniSuper's private cloud: the blank parameter, the unknown one-year term, the 24x7 multi-day recovery, and the sentence crediting the customer's third-party backup software as instrumental.

Carry forwardYour provider's control plane is a single failure domain spanning all your regions. Hold one copy beyond it.
cloud.google.com
Design doc resticchecked 2026-09

restic: doc/design.rst

The repository format's threat model in one line: "The host system a backup is created on is trusted", the storage is not, and everything in the format follows from making integrity checkable against untrusted storage.

Carry forwardDecide explicitly which side of the backup you trust; the design of verification follows from that answer.
restic/restic
Documentation resticchecked 2026-09

restic: checking integrity and consistency

The default check is structural; verifying actual data needs --read-data, which downloads every pack file, and the docs say so with a bandwidth warning. Sampling via --read-data-subset, by fraction or percentage, is the documented compromise.

Carry forwardFull verification has a cost curve; write the sampling schedule down before the repository grows past affording it.
restic/restic
Documentation PostgreSQLchecked 2026-09

PostgreSQL: backup.sgml (continuous archiving)

The constraints that shape every serious deployment: file-system backups restore the whole cluster or nothing; WAL archiving must be monitored or it silently loses its guarantee; corrupted WAL ends recovery early at an implicit target.

Carry forwardPITR's precision is bounded by the health of the archive chain; monitor the chain, not the base backup.
postgres/postgres
Documentation etcdv3.5 docs, checked 2026-09

etcd: snapshot and restore

Restoring is documented as a semantic event, not a copy: clients can observe the revision going backwards, Kubernetes informers may hold caches the restore invalidates, and a snapshot taken by copying the db file can miss unflushed WAL.

Carry forwardPlan the re-entry: what do clients, caches and downstream systems do when the store's clock rolls back?
etcd-io/website
Source GitHubchecked 2026-09

github/backup-utils

GitHub Enterprise Server's backup tooling: incremental snapshots transferring only changes, configurable retention, parallel backup and restore, and a deprecation notice as backup moves into the product ("backup-service becomes GA in GHES 3.20").

Carry forwardBackup tooling migrates like any other dependency; a deprecation on the backup path is an action item with a date.
github/backup-utils
Design doc Velerochecked 2026-09

Velero: restore progress reporting design

The design opens by recording the asymmetry it fixes: backups had real-time progress, restores exposed only a phase. The operator watching a long restore had no way to distinguish progress from a hang.

Carry forwardA restore without progress observability becomes an unmeasurable RTO the first time it is slow.
vmware-tanzu/velero
Design record Kuberneteschecked 2026-09

KEP-177: CSI volume snapshots

The platform's snapshot primitive, with the consistency contract stated in passing: the application "is usually quiesced before taking a snapshot", which is to say the platform snapshot is crash-consistent and application consistency is your hook to write.

Carry forwardKnow which consistency your snapshots actually promise; test restores of a busy workload, not an idle one.
kubernetes/enhancements
07

Build a miniature, then productionise it

Seven rungs from an evening's toy to the shape GitLab runs. The line from toy to real is crossed at rung 4, where a machine starts proving the restore without you.

Back up and restore by hand, once

Run a database with sample data, take a logical dump, destroy the database, restore into a fresh instance and diff row counts per table.

Done when: the diff is empty and you wrote down how long each step took.  Teaches: the restore path has more steps than the backup path, and the timings are not symmetric.

Add continuous archiving and hit a point in time

Enable WAL archiving to object storage (any S3-compatible store), take a base backup, write for an hour, then restore to a timestamp ten minutes before a deliberate bad write.

Done when: the bad write is absent and everything before it is present.  Teaches: RPO is a property of the archive chain, and the docs' warning about monitoring that chain becomes concrete the first time a segment is missing.

Break the restore, not the backup

Upgrade the database one major version and re-run yesterday's restore procedure unchanged. Then downgrade the client tools and try again.

Done when: you have seen a green backup fail to restore for a reason that no checksum would catch.  Teaches: the failure class behind GitLab's 2017 and 2026 incidents, on your own machine.

Put the restore in CI with a dead-man switch

A scheduled pipeline restores the latest backup into a fresh instance, runs three integrity queries, and pings a snitch endpoint on success. Alert when the ping is absent, not when the job reports failure.

Done when: stopping the backup job pages you within one schedule interval, with nobody watching.  Teaches: alert-on-silence, which is the load-bearing inversion in the whole architecture.

Sample-verify the bulk store

For file or snapshot data, restore one randomly chosen artifact per cycle and check freshness (GitLab checks a restored Gitaly disk for recent commits). For a restic repository, schedule read-data-subset across the week.

Done when: the sampling schedule covers the corpus in a bounded window and the verification cost appears in your bill.  Teaches: verification depth as a budgeted quantity, not a virtue.

Move a copy out of your blast radius

Replicate the backup bucket to a different account with different credentials, or a different provider. Then run a drill assuming your primary account is hostile: restore using only the second copy.

Done when: the restore succeeds from credentials your primary admin cannot revoke.  Teaches: the GCVE lesson, that replication inside one administrative scope is one copy.

Run a gameday and grade yourself

Simulate losing a zone or the primary datastore, execute the runbook end to end with a colleague as reviewer, record every timing, and grade each service on GitLab's four-level confidence scale.

Done when: the measurements are committed next to the runbook and at least one service moved up a confidence level.  Teaches: RTO as a measured, dated number, and how much of it is humans coordinating rather than bytes moving.

08

Keep hunting

The queries that found this material. The first group works on any GitLab-hosted operator record; the third generalises to any organisation that runs its infrastructure in the open.

Incident archaeology on gitlab.com

  • https://gitlab.com/api/v4/projects/7444821/issues?labels=incident&search=backup
  • https://gitlab.com/api/v4/projects/<id>/issues?search=outage&created_after=2017-01-25&created_before=2017-03-01
  • https://gitlab.com/api/v4/groups?search=gitlab-restore
  • https://gitlab.com/api/v4/projects/<id>/merge_requests?state=closed&search=backup

Recovering deleted primary sources

  • /repository/commits?path=source/posts&since=2017-02-09&until=2017-02-12
  • /repository/files/<urlencoded-path>/raw?ref=<old-sha>
  • git clone --depth 1 --filter=blob:none --sparse <repo> && git sparse-checkout set design doc

The operator record, any org

  • site:gitlab.com runbooks backup restore testing
  • "dead man's snitch" OR "snitch missing" backup restore incident
  • "gameday" OR "game day" "disaster recovery" runbook measurements
  • "restore" "we found" backup "was empty" postmortem

Tool design records

  • repo design/ directory: velero, restic doc/design.rst, keps/sig-storage
  • "--read-data" OR "read-data-subset" site:github.com restic
  • "exactly the same version" restore site:docs.gitlab.com
09

References

  1. GitLab, Postmortem of database outage of January 31 (markdown source at 2017 ref) gitlab.com repository record, 2017-02-10. Checked 2026-09-11.
  2. GitLab infrastructure issue #1102, Automated testing of recovering PostgreSQL database backups gitlab.com, 2017-02-01. Checked 2026-09-11.
  3. GitLab infrastructure issue #1168, Backup system and replication is not working gitlab.com, 2017-02-11. Checked 2026-09-11.
  4. GitLab infrastructure issue #1161, Disaster recovery for everything that is not the database gitlab.com, 2017-02-10. Checked 2026-09-11.
  5. GitLab runbooks, Disaster Recovery README gitlab.com/gitlab-com/runbooks. Checked 2026-09-11.
  6. GitLab runbooks, Gamedays gitlab.com/gitlab-com/runbooks. Checked 2026-09-11.
  7. GitLab runbooks, Measuring Recovery Activities gitlab.com/gitlab-com/runbooks, entries 2024-2026. Checked 2026-09-11.
  8. GitLab runbooks, PostgreSQL backups with WAL-E / WAL-G gitlab.com/gitlab-com/runbooks, figures as of 2020-2021. Checked 2026-09-11.
  9. GitLab handbook, Backups of GitLab.com (controlled document) gitlab.com/gitlab-com/content-sites/handbook. Checked 2026-09-11.
  10. GitLab handbook, Disaster Recovery of GitLab.com (controlled document) gitlab.com/gitlab-com/content-sites/handbook. Checked 2026-09-11.
  11. GitLab production incident #4056, Our database backups are failing to verify gitlab.com, 2021-03-24. Checked 2026-09-11.
  12. GitLab production incident #3338, database failover triggered by GCP snapshots gitlab.com, 2021-01-16. Checked 2026-09-11.
  13. GitLab production incident #22543, gitlab-restore/postgres-gprd snitch missing gitlab.com, 2026-07-20. Checked 2026-09-11.
  14. GitLab MR !231770, Remove --unlink-first and --recursive-unlink from tar extract gitlab.com, 2026-04-16, closed unmerged. Checked 2026-09-11.
  15. GitLab MR !229743, Add Barman backup option for large database gitlab.com, 2026-03-31, closed unmerged draft. Checked 2026-09-11.
  16. GitLab documentation, Restore GitLab gitlab.com/gitlab-org/gitlab. Checked 2026-09-11.
  17. GitLab, gitlab-restore group gitlab.com, group created 2019-01-08. Checked 2026-09-11.
  18. Google Cloud, Details of Google Cloud GCVE incident cloud.google.com, 2024-05-24. Checked 2026-09-11.
  19. restic, Repository design document github.com/restic/restic. Checked 2026-09-11.
  20. restic, Working with repositories: checking integrity and consistency github.com/restic/restic. Checked 2026-09-11.
  21. PostgreSQL, Backup and Restore documentation source (backup.sgml) github.com/postgres/postgres. Checked 2026-09-11.
  22. etcd, Disaster recovery (v3.5 operations guide) github.com/etcd-io/website. Checked 2026-09-11.
  23. GitHub, backup-utils README github.com/github/backup-utils. Checked 2026-09-11.
  24. Velero, Restore progress reporting design github.com/vmware-tanzu/velero. Checked 2026-09-11.
  25. Kubernetes, KEP-177: CSI Volume Snapshot github.com/kubernetes/enhancements. Checked 2026-09-11.