What is restore testing?
Restore testing is the practice of verifying that a backup can be fully recovered into a working state, not just confirming the backup was created. For git repositories, this means extracting the backup to a clean environment and confirming that the object database passes git fsck, all branches and tags are present, the full commit history is intact, and any uncommitted work (staged changes, stashes, untracked files) was captured. A backup that has never been restore-tested is an assumption, not a guarantee.
Restore testing means actually trying to open your backup and use it, not just trusting that the backup was made. Most people save backups but never check if they can get their project back from one.
Restore testing is the practice of exercising the full read path of a backup: extracting to an isolated environment, validating object-store integrity (git fsck), confirming ref-graph completeness (branches, tags, reflog), and verifying uncommitted state capture (index, working tree, stash stack, operation state). It is the difference between "backup exists" and "backup is restorable."
The asymmetry
Every developer has backups. Almost none of them have tested a restore.
This is the backup/restore asymmetry: creating a backup is easy, fast, and gives you a green checkmark. Restoring from that backup is hard, slow, and most people never try it until the day they actually need it. By then, it is too late to find out the backup was incomplete, corrupted, or locked behind a vendor you can no longer reach.
The backup succeeds silently. The restore fails loudly. And it fails on the worst possible day, when your machine is dead, your disk is gone, or your repository is corrupted beyond repair.
A backup you have never restored is not a backup. It is a hope.
Most backup tools are optimized for the backup side of this equation. They report success when the upload completes. They count bytes transferred. They log timestamps. What they do not do is prove that the data on the other end can be turned back into a working repository, with every file, every commit, every branch, and every piece of uncommitted work intact.
Everyone has backups. Almost nobody has ever tried restoring from one.
Backing up is the easy part. It runs in the background, shows a green checkmark, and you forget about it. Restoring is the hard part, and most people never try it until the day they actually need it.
Most backup tools tell you the upload finished. They count how many files were saved. But they never prove that you can actually get your project back from that backup. The green checkmark means "saved," not "safe."
The backup/restore pipeline has a fundamental asymmetry: the write path is fast, automated, and well-instrumented. The read path (restore) is slow, rarely tested, and typically only exercised during an incident.
Backup: automated, fast, green checkmark, runs daily
Restore: manual, slow, untested, runs once (during an incident)
Most backup tooling optimizes for the write path and ignores the read path entirely.
Standard backup instrumentation (bytes transferred, upload ACK, timestamp logged) validates the write path. None of these metrics prove the read path works. The data on the remote end may be incomplete, internally inconsistent, or locked behind a dependency that will not survive the same failure that triggered the restore.
Common failure modes
Backup tools fail restores in predictable ways. These are the categories we see most often.
Filesystem-level backup misses git internals
The most common mistake is treating a git repository like a folder of files. Tools that copy the working directory get the files you can see, but miss the data that makes git work: the object database, the pack files, the refs, the reflog, the hooks, the submodule configurations. A filesystem copy of a repository is not a repository. It is a snapshot of one moment's checkout.
Try restoring a filesystem copy and running git log. If the object database was not captured correctly, you will get errors. If the pack files were copied mid-write, you will get corruption. If the refs were copied out of order, branches will point to commits that do not exist. The files are there. The history is gone.
Cloud sync services (iCloud, Dropbox, OneDrive) are filesystem-level tools. They copy files as they see them, with no awareness of git's internal structure. This is the documented source of .git directory corruption that affects thousands of developers.
Incremental backup chains break
Incremental backups save space by only storing what changed since the last backup. This is efficient on the backup side. On the restore side, it means every restore depends on an unbroken chain of increments going back to the last full backup.
If any link in that chain is corrupted, missing, or inaccessible, the restore fails. Not partially. Completely. One bad increment in a chain of 200 means all 200 are useless. The more increments you have, the more fragile the restore becomes. The backup tool shows "199 successful backups." The restore shows "chain broken at increment 47."
Encrypted backups with lost keys
Encryption is necessary. But encryption without key management is a time bomb. If the key that encrypted your backup is stored on the same machine the backup is protecting, you lose both at the same time. If the key is stored in a password manager that requires a working machine to access, same problem.
The worst version of this: the backup tool manages the key for you, stored on the vendor's servers. Your backup is encrypted. Your key is held by someone else. If that vendor goes down, gets acquired, or changes their terms, your encrypted backup becomes an encrypted brick.
Vendor-dependent restore processes
Some backup tools require their own software, their own servers, or their own authentication to restore. This creates a dependency chain: to get your data back, you need the vendor to be online, authenticated, and running the same version of their software that created the backup.
This is the worst possible architecture for an emergency. Emergencies happen when things are broken. If your restore process requires a functioning internet connection, a running vendor service, and a valid subscription, you have added three new failure modes to the moment when you can least afford them.
Missing uncommitted work
Most backup tools that are git-aware will capture committed data. But the work developers lose most painfully is the work they had not committed yet: the staging area, the working directory changes, the stashed experiments, the in-progress merge or rebase.
A backup that captures only committed history is a backup of last week. The hours of work sitting in your working directory, not yet committed, are gone. And those are the hours that hurt the most, because they are the hours you remember doing.
Backups fail in a few predictable ways. Here are the ones that catch people most often.
Copying files is not the same as copying a project
Most backup tools just copy the files they can see. But your project has hidden internal data that tracks all your version history. If that internal data is missing or was copied while it was being updated, you get the files back but lose everything else.
Chain backups are fragile
Some tools save space by only backing up what changed. Sounds smart. But it means every restore depends on every previous backup in the chain being perfect.
If any single link in that chain is missing or damaged, the whole thing breaks. Not partially. Completely. The tool says "199 successful backups." The restore says "broken."
Encrypted backups with lost passwords
Encryption keeps your backups private. But if your password is stored on the same laptop that died, you lost both your data and the key to unlock the backup at the same time.
Restoring requires the company to be online
Some backup tools need their own servers to be running before you can restore. If that company goes down, gets bought, or changes their pricing, your backup is locked behind a door someone else controls.
Your unsaved work is gone
Most backup tools save your committed work. But the changes you were in the middle of? The experiments you stashed? The work you did today but had not committed yet? Those are usually gone.
Backup restore failures cluster into five categories. Each is predictable and preventable.
F1: Filesystem-layer capture (wrong abstraction)
Root cause: Backup operates on file tree, not git object graph
Symptom: Files present, object DB invalid, git fsck fails
Affected tools: iCloud, Dropbox, OneDrive, rsync, cp -r
F2: Incremental chain fragility
Root cause: Restore depends on unbroken increment chain
Symptom: Single corrupted/missing increment invalidates entire chain
Risk scales linearly with chain length
F3: Key management failure domain overlap
Root cause: Decryption key stored in same failure domain as encrypted data
Variant: Vendor-managed keys create external dependency on restore path
F4: Vendor-coupled restore path
Root cause: Restore requires vendor auth, vendor servers, vendor software version
Failure mode: Vendor outage/acquisition/EOL blocks restore at worst possible time
F5: Uncommitted state loss
Root cause: Backup captures committed refs only, skips index/worktree/stash/operation state
Impact: Highest-value work (in-progress changes) is the work not captured
The verification gap
There is a fundamental difference between two things that most backup tools conflate:
- "Upload succeeded" means bytes were transferred to a destination and the destination acknowledged receipt.
- "Restore will work" means those bytes, when retrieved and reassembled, will produce a complete, functional, verified copy of the original data.
Most backup tools only check the first one. They verify that the upload completed, that the file size matches, maybe that a checksum passed. Then they report success.
But upload success does not mean restore success. The upload could have succeeded while the data was already corrupted on disk. The checksum could match a corrupted file. The backup could be complete but missing a critical piece of metadata that the restore process needs. The archive format could be valid but the contents could be internally inconsistent.
A checksum proves the file was not altered in transit. It does not prove the file was correct before transit. If you back up a corrupted pack file, the checksum will pass. The backup succeeded. The data is still corrupted.
This gap is invisible during normal operations. Every backup reports green. Every log shows "completed." The gap only becomes visible on the day you try to restore, and by then you are staring at the consequences of every assumption the tool made on your behalf.
There is a gap that most backup tools hide from you. They check one thing but call it something else.
Your backup tool might show a green checkmark. But that checkmark only means the file was saved. It does not mean the file is usable. The data could have been broken before it was saved, and the checkmark would still be green.
This gap is invisible until the day you need to restore. Every backup looks green. Every log says "completed." You only find out there is a problem when it is too late to fix it.
Most backup tools conflate two distinct assertions:
ASSERTION A: "Upload succeeded" = bytes transferred, ACK received
ASSERTION B: "Restore will work" = bytes can reconstruct a valid, complete, functional state
A does not imply B. Most tools only check A.
The gap between these assertions is where silent failures accumulate. A checksum validates transfer integrity, not source integrity. A valid archive container can hold corrupted contents. A complete upload can miss application-level metadata required for restore.
When backups failed for real
GitLab, January 2017
On January 31, 2017, a GitLab engineer accidentally deleted a production database directory. This is a bad day, but it should not be a catastrophic one. Backups exist for exactly this scenario.
GitLab had five independent backup methods running:
- Regular database dumps (pg_dump): Had not been working. Silently failed due to a configuration error. No one noticed.
- Automated snapshots (Azure): Not configured for the production database server.
- Continuous archiving (WAL-E): Also not configured for the production database server.
- LVM snapshots: Were being taken, but the process was inconsistent and the snapshots were untested.
- Disk snapshots (Azure): The only method that partially worked. They recovered from a snapshot that was six hours old, losing six hours of production data across all of GitLab.com.
Five backup methods. Four completely failed. One was six hours stale. None had been tested with a real restore. GitLab, to their enormous credit, live-streamed the recovery and published a detailed post-mortem. Most companies would not be that transparent. But the lesson is stark: the number of backup methods is irrelevant if none of them have been tested with an actual restore.
The post-mortem revealed that the regular database dumps had been silently failing for months. The tool reported no error. It simply did not produce a usable backup. Nobody checked because the process was assumed to work.
The Time Machine discovery
A pattern we hear repeatedly from developers: Time Machine shows a full backup history, green checkmarks, years of data. Then the hard drive fails. They go to restore. And they discover one of several problems:
- The Time Machine backup drive itself has disk errors, and the backup is partially corrupted.
- The backup completed but certain directories were excluded by a rule the developer did not set (or did not remember setting).
- The
.gitdirectory was backed up, but it was backed up mid-operation, with locked index files and partially written pack files. The repository is technically present but internally inconsistent. - The backup is fine, but the restore takes 14 hours over USB, and the developer needed to ship code today.
Time Machine is a good tool. It does what it says it does. But it is a filesystem-level tool. It does not understand git internals, it does not verify repository integrity, and it does not distinguish between "all the files are there" and "the repository actually works."
GitLab lost 6 hours of data (2017)
GitLab is one of the biggest platforms for hosting code projects. In 2017, an engineer accidentally deleted a production database.
They had five different backup systems running. All five showed green checkmarks. But when they actually tried to restore:
The scariest part? One of the backup systems had been silently failing for months. It looked fine. It reported no errors. It was not producing usable backups. Nobody checked because everybody assumed it worked.
Time Machine surprises
We hear this story a lot: a developer has Time Machine running for years. Green checkmarks. Everything looks great. Then their hard drive dies. They try to restore and find:
- The backup drive itself has errors, so the backup is partially broken.
- Some folders were excluded by a setting they did not remember changing.
- The project files are there, but the version history is broken because Time Machine copied it mid-operation.
- The restore takes 14 hours and they needed to ship code today.
GitLab Production Database Deletion (2017-01-31)
Backup Methods: 5 | Functional on Restore: 1 (partial)
pg_dump: SILENT FAIL (config error, months undetected)
Azure snapshots: NOT CONFIGURED for prod DB
WAL-E archiving: NOT CONFIGURED for prod DB
LVM snapshots: INCONSISTENT, untested
Azure disk snapshots: 6hr RPO (only working method)
Data loss: 6 hours of production data across GitLab.com
Root cause: zero restore verification across all five backup methods. The pg_dump pipeline had been silently failing for months with no alerting. The backup monitoring checked "did the job run" but not "did it produce a restorable artifact."
Time Machine: Filesystem-Layer Limitations
Recurring pattern in developer incident reports: Time Machine copies the .git directory as a filesystem artifact, not as a semantic structure. Results on restore:
- Backup drive degradation produces partially corrupted archives
- Exclusion rules silently omit directories
- Mid-operation copies produce locked index files and partially written pack files (structurally invalid repository)
- Restore time exceeds RTO (14+ hours over USB)
What a real restore test looks like
A real restore test is not "can I download the backup file." A real restore test answers the question: if my machine was gone right now, could I get back to a working state from this backup alone?
Here is what that actually requires:
- Restore to a clean machine. Not the same machine. Not a directory next to the original. A machine with nothing on it. If the restore depends on something already present on your system, it is not a complete backup.
- Verify every file. Not just that files exist, but that their contents match. Byte-for-byte comparison against the original. If even one file differs, find out why.
- Check git history. Run
git log,git branch -a,git stash list. Every commit, every branch, every tag, every stash should be present. If the backup only captured the current branch, the rest of your history is gone. - Run the tests. If your project has tests, run them. A restore that produces files but breaks the build is not a successful restore. Dependencies, configurations, environment variables, build artifacts: all of these matter.
- Check uncommitted work. Was there anything in your working directory that was not committed? Was there anything in the staging area? Stashed changes? An in-progress rebase? These are the things most backup tools miss entirely.
- Measure the time. How long did the restore take? If it takes 8 hours to restore, and your deadline is in 4, the backup exists but is not operationally useful.
Most developers have never done this. Most backup tools have never been subjected to this. And until you do it, every "backup succeeded" message is a statement of faith, not a statement of fact.
# A minimal restore test checklist
# Run this against your restored repo, not the original
# 1. Does the repo exist and have valid objects?
git fsck --full
# 2. Is the complete history present?
git rev-list --count --all
# 3. Are all branches present?
git branch -a
# 4. Are all tags present?
git tag -l
# 5. Are stashes present?
git stash list
# 6. Does the reflog exist?
git reflog show HEAD
# 7. Do the tests pass?
make test # or: npm test, go test ./...
A real restore test is not just downloading a file. It answers one question: if your laptop was gone right now, could you get back to exactly where you were?
The restore test checklist:
- Use a clean location. Do not restore next to the original. Use a different folder or machine. If the restore needs something already on your computer, it is not a real backup.
- Check every file. Make sure the files are actually there and their contents match. Not just the names.
- Check your history. Make sure all your branches, tags, and past versions are still there. If only the current version was saved, the rest is gone.
- Run your project. Can it build? Can it run? A backup that gives you files but breaks your project is not really a backup.
- Check unsaved work. Did it capture the changes you had not committed yet? Those are usually the most painful to lose.
- Time it. How long does the restore take? If it takes all day and you need to ship in two hours, the backup is not useful enough.
A restore verification is not a download check. It is a proof that the backup can reconstruct a fully operational repository in an isolated environment.
Restore Verification Steps
1. ISOLATE: Extract to a clean environment (ephemeral container or fresh VM). No shared state with the source.
2. INTEGRITY: Run object store validation (git fsck --full). Zero tolerance for dangling or corrupt objects.
3. COMPLETENESS: Diff branch set, tag set, and commit count against source manifest. Any delta = failure.
4. BUILD: Execute the project's test suite. Files present but build broken = restore failure.
5. UNCOMMITTED: Verify index state, working tree diff, stash stack, and operation state capture.
6. SLA: Measure wall-clock restore time. Exceeding RTO = operationally invalid backup.
Backup verification vs. restore verification
These are two different things. Most tools do the first. Almost none do the second.
Backup verification answers: "Did the backup process complete without errors?" It checks that the file was created, that the upload finished, that the size is nonzero, that a checksum matches. This is necessary but not sufficient.
Restore verification answers: "Can this backup be turned back into the original data?" It proves that the backup is not just present but functional. That the archive can be opened. That the data inside is consistent. That the git objects form a valid graph. That the files match the originals. That nothing was silently dropped.
The difference matters because there are entire categories of failure that pass backup verification but fail restore verification:
- A zip file that was created successfully but contains a corrupted pack file inside it. The zip is valid. The pack file is not.
- An encrypted archive where the encryption succeeded but the key needed for decryption is stored in a location that will not survive the same failure that triggered the restore.
- An incremental backup that uploaded correctly but depends on a base snapshot that has already been deleted by a retention policy.
- A backup that captured the committed history but silently skipped the working directory because the tool treated uncommitted files as temporary.
Every one of these shows "backup succeeded" in the logs. Every one of these fails on restore day.
The goal is not to confirm that bytes exist on a server. The goal is to prove that those bytes can become a working repository again. Any tool that stops at the first goal is leaving the hardest part of the problem unsolved.
These sound like the same thing, but they are different in ways that matter.
Here are real ways a backup can "succeed" but still fail you:
- The backup file was saved, but something inside it was already broken before it was saved.
- The backup is encrypted, but the password is stored on the same laptop that died.
- The backup depends on older backups that were automatically deleted.
- The backup saved your committed work but skipped everything you had not committed yet.
Two distinct verification stages exist in backup pipelines. Most implementations only cover the first.
Stage 1: Backup Verification (write-path)
Checks: upload ACK, byte count, checksum match
Proves: data was transferred
Does NOT prove: data is restorable
Stage 2: Restore Verification (read-path)
Checks: deserialization, object graph validity, ref completeness, content integrity
Proves: data can become a working repository
Required for: production DR guarantees
Failure modes that pass Stage 1 but fail Stage 2:
- Corrupted pack file inside a valid archive container
- Encryption key stored in the same failure domain as the data it protects
- Incremental chain broken by retention policy deleting the base snapshot
- Committed history captured, uncommitted state silently dropped
What good looks like
A backup system that actually protects you has several properties that most tools lack:
- Git-aware capture. It reads directly from git, not the filesystem. The backup is a valid git artifact, not a folder of files.
- Uncommitted work included. It captures everything in your working state: the staging area, the working directory, stashed changes, in-progress operations. The work you have not committed yet is the work you will miss the most.
- Restore verification built in. It does not just confirm the upload. It proves that the backup can be restored. It verifies the data after encryption, after upload, and before you need it. Every backup is tested before it is trusted.
- No vendor dependency on restore. You can restore without the backup tool's servers being online. You can restore without a subscription. The backup format is something you can open with standard tools if the vendor disappears. Your data belongs to you, and you can get to it with or without anyone else's cooperation.
- Keys you control. Encryption keys stay on your machine. They are not stored by the vendor. They are not managed by a service you do not control. If you lose the key, that is your responsibility, but at least it is your responsibility and not a vendor's business decision.
- Storage you own. The backup goes to storage you control: your own S3-compatible bucket, your own infrastructure. Not a vendor's proprietary storage where your data lives at the vendor's discretion.
These are not exotic requirements. They are the minimum for a backup system that actually works on the day you need it. The problem is that most tools skip the hard parts (restore verification, uncommitted work capture, vendor-independent restore) because those are the parts that require understanding git at the object level, not the filesystem level.
The question is simple: if your machine died right now, could you get back to exactly where you were? Not last week's committed code. Not yesterday's push to remote. Right now. Every file, every branch, every uncommitted change.
If the answer is "I think so," that is not good enough. Test it. Find out. The time to discover your backup does not work is today, not the day you actually need it.
A backup system that actually has your back looks like this:
- It understands your project. It does not just copy files. It reads the version history, the branches, all of it.
- It saves your unsaved work. The changes you have not committed yet? Those get captured too.
- It proves the backup works. Not just "upload complete." It actually tests that your project can be restored.
- It works without the company. If the backup tool company goes away, you can still open your backups. No lock-in.
- You hold the keys. Your encryption keys stay on your machine. Nobody else can read your backups.
- Your storage, your rules. Backups go to a cloud bucket you own. Not someone else's server.
A backup system suitable for automated environments must satisfy these properties:
Required Properties
PROP-1: Git-semantic capture (object DB, not filesystem)
PROP-2: Uncommitted state capture (index + worktree + stash + operation state)
PROP-3: Post-backup restore verification (automated, not manual)
PROP-4: Vendor-independent restore path (standard formats, no auth dependency)
PROP-5: Client-side key management (keys never leave the machine)
PROP-6: User-owned storage (S3-compatible, no proprietary backend)
Frequently asked questions
Why do most backup tools fail during a restore?
Most backup tools treat git repositories as collections of flat files instead of understanding the internal object database. During restore, this means pack files, refs, and the object store can be incomplete or inconsistent, causing git errors even though the file copy appeared successful.
How do you test if a backup is actually restorable?
A real restore test means extracting the backup to a clean directory and running git fsck to verify object integrity, git log to confirm full commit history, git branch -a to check all branches survived, and git stash list to verify uncommitted work was captured. If any of those fail, the backup is incomplete.
What is the difference between backup verification and restore verification?
Backup verification confirms data was uploaded successfully, typically by checking byte counts or checksums of the transferred files. Restore verification proves the data can be turned back into a working repository with full history, branches, and uncommitted work intact. Most tools only do backup verification.
Why would my backup not work when I need it?
Most backup tools just copy your project files like a regular folder. But your project has invisible internal data that tracks its full history. If that internal data is missing or broken, you get the files back but lose everything that makes version control work.
How do I know if my backup actually works?
The only way to know is to try restoring it. Open the backup on a different machine (or a clean folder), make sure all your files are there, make sure your full history is there, and make sure any work you had not saved yet was captured. If you have never tried this, you do not know if your backup works.
What is the difference between "backup worked" and "restore works"?
"Backup worked" means the file was saved somewhere. "Restore works" means you can actually open it and get your full project back, with all your history and unsaved changes. Most tools only check the first one.
Why do backup-then-restore pipelines fail at the restore stage?
Most backup implementations operate at the filesystem layer, treating a git repository as a flat directory tree. This misses the internal object graph, pack file integrity constraints, and ref consistency requirements. On restore, the filesystem-level copy produces a structurally invalid repository that fails git fsck even though every file was transferred correctly.
What does a production-grade restore verification look like?
A restore verification pipeline extracts the backup to an isolated environment, runs git fsck --full against the object store, validates ref graph completeness via git rev-list --all, diffs branch and tag sets against a manifest, and confirms uncommitted state capture (index, working tree, stash stack). This should run as a post-backup CI stage, not a manual process.
How does backup verification differ from restore verification in automated pipelines?
Backup verification is a write-path check: bytes transferred, checksum matched, storage acknowledged. Restore verification is a read-path proof: data can be deserialized into a valid, consistent, complete repository. The gap between these two is where silent data loss lives. Production systems need both stages in their backup pipeline.