Garage S3: LMDB Corruption Post-Mortem
Complete loss of Garage metadata after a power outage on Btrfs
Yesterday I was migrating my data from an rclone-mounted bucket to a proper OpenCloud setup backed by my home Garage S3 cluster. Things were going smoothly: files copying over, users importing, the new stack finally taking shape.
Then the power went out.
When the server came back, Garage refused to start. The LMDB metadata database was completely corrupted, rendering the cluster inoperable. The block data (781k objects, ~57 GB) remained intact on a Btrfs RAID1, but without the LMDB index the mapping from S3 keys to block hashes was lost. The data in my personal bucket and the one used by OpenCloud as its storage backend: gone.
It could have been worse. My most important data lives outside S3, backed by restic to a different target, and that backup pipeline was unaffected. Still, losing two buckets stings.
That said, I didn’t choose S3 on a whim. This bucket needed to be accessed from both my homelab and several VPSs hosting public services, a mix of edge and cloud. S3 gives me a single consistent API and authentication layer everywhere, and Garage’s S3 gateway exposes it without vendor lock-in (unlike, cough, certain other open-source S3 implementations). A local filesystem wouldn’t solve that. I like the option to add nodes for replication down the road, but a crash like this makes you question whether the complexity is worth it for a single-node deployment.
TL;DR: LMDB is known to be vulnerable to corruption after an unclean shutdown. Garage provides a built-in snapshot mechanism (
metadata_auto_snapshot_interval) exactly for this scenario. I hadn’t enabled it. I also had no Btrfs snapshots on the metadata partition. If you run Garage on LMDB, enable snapshots today.
System Layout
- Metadata LMDB: single-device Btrfs partition (
/dev/nvme0n1p2, 238 GiB NVMe, 98% full) - Block data: Btrfs RAID1 (
/mnt/raid/, 5.5 TiB, HDDs) - Garage version: v2.x,
db_engine = "lmdb" - COW: enabled on both partitions
- Snapshots/backups: none for the metadata directory
Timeline
- T-unknown: Power outage. Server loses power mid-operation. Garage was writing to the LMDB database.
- T=0: Server boots back up. Garage refuses to start. Attempting to inspect the LMDB file:
$ mdb_stat -e /var/lib/garage/meta/db.lmdb/data.mdb
mdb_stat: src/mdb.c:...: Assertion 'IS_BRANCH(mc->mc_pg[mc->mc_top])' failed.
Aborted
$ mdb_copy -c /var/lib/garage/meta/db.lmdb/data.mdb /tmp/recovery.mdb
mdb_copy: src/mdb.c:...: Assertion 'IS_BRANCH(mc->mc_pg[mc->mc_top])' failed.
Aborted
The LMDB b-tree is structurally gone. I sat there staring at the terminal for a good minute. No error message to decode, no partial recovery: just a dead database and 57 GB of unlabeled blocks on the RAID. Garage’s own docs warn about this:
LMDB is prone to database corruption after an unclean shutdown (e.g. a process kill or a power outage).
- T+1h: Garage service migrated to a fresh cluster on a different machine. Block data copied from the RAID. OpenCloud is re-targeted.
- T+2h: Btrfs COW recovery attempted. All 10 candidate tree roots (generations 2477737–2477773) contain the same corrupt extents.
- T+4h: LMDB raw page scan. 956k pages, all
flags=0x0000,lower=1,upper≈24. No valid root page. The database was effectively erased. - T+24h: Recovery abandoned. Data re-seeded from scratch where possible.
Root Cause
A power outage interrupted Garage mid-write on the LMDB metadata database. LMDB uses mmap’d I/O and, by default, operates with MDB_NOMETASYNC and MDB_NOSYNC (Garage sets metadata_fsync = false by default). Without fsync, a power loss during a transaction commit leaves the b-tree in an inconsistent state: torn pages, missing branches, zero-filled root pointers.
This is a well-documented limitation of LMDB, not a Garage bug. The recovery path Garage documents is:
- Replication (requires
replication_factor ≥ 2and multiple nodes): delete the corrupt DB, restart, and rungarage repair -a --yes tablesto resync from peers. - Metadata snapshots (
metadata_auto_snapshot_interval): restore from a Garage-generated snapshot, then repair. - Filesystem snapshots (Btrfs/ZFS): restore from an old snapshot of the metadata directory.
I had none of these. I didn’t know about the snapshot feature; I hadn’t read the docs carefully enough. If I had, this post wouldn’t exist.
Recovery Attempts
Btrfs COW recovery
The LMDB file had COW enabled. Each page write creates a new extent at a different physical location; old page data remains on disk until overwritten.
$ btrfs-find-root /dev/nvme0n1p2
Well block 174354268160 (gen: 2477773)
Well block 174354300928 (gen: 2477772)
...
Found tree root at 174435041280 gen 2477773
Found tree root at 174433533952 gen 2477772
Found tree root at 172099076096 gen 2477739
...
10 candidate tree roots identified, spanning generations 2477737–2477773.
Each root was walked to find EXTENT_DATA items for the LMDB inode. The extent map had not changed significantly between generations; only the final extent was COW’d at gen 2477738, and all earlier extents pointed to the same (corrupt) physical blocks.
$ btrfs restore -t <root> /dev/nvme0n1p2 /tmp/recovery/
# btrfs restore cannot traverse FS_TREE roots at b-tree level 3
Direct file extraction from old generations was not possible.
$ btrfs restore -r /dev/nvme0n1p2 /tmp/recovery/
# Produces the same corrupt file; the inode's extent map had not reverted
Conclusion: The corruption happened before any Btrfs snapshot could diverge. The old page data was in Btrfs free space with no index to locate it. COW alone is not a backup. You need explicit snapshots.
LMDB raw page analysis
A custom scanner was written to walk the LMDB file and recover any valid b-tree pages. This felt promising at first: surely some pages survived. Scanning through the raw bytes, looking for recognizable LMDB page headers, hoping for a valid root.
This is exactly the kind of task where AI tools shine. You have a stable read-only copy of the corrupt file, a well-defined binary format, and a one-off analysis script you’ll never need again. Perfect for throwing at an LLM and iterating on the output.
Total pages: ~956,000
Leaf pages: 983
Branch pages: 304
Overflow: 1,938
Root pages:
Active root at position 956,702 → points to zero-filled page
Last root at position 956,708 → points to zero-filled page
Freelist:
freeid counters: 1, 2 → fully-allocated file, no released pages
All pages:
flags = 0x0000
lower = 1
upper ≈ 24 ← metadata pointer structure erased
The corruption pattern is consistent with mdb_env_open(MDB_CREATE) initializing a new empty database over the existing file. The file was re-opened after the crash with no readable b-tree root.
This is a deliberate LMDB design choice: MDB_CREATE makes mdb_env_open silently create or overwrite the database file if the header is unreadable, rather than returning an error. The rationale is performance: LMDB avoids a full integrity scan at startup and trusts the OS to have flushed pages correctly. If you used metadata_fsync = true, LMDB would have at least synced the meta page, giving the header enough integrity to survive a crash. But with the default false and MDB_NOSYNC in play, a torn meta page looks exactly like an uninitialized file, and LMDB’s fail-safe is to start fresh rather than halt. It’s a speed-over-safety tradeoff that makes sense for the read-optimized workloads LMDB was built for, but backfires catastrophically when power is lost mid-write.
Orphaned block extraction (not executed)
781k orphaned zstd-compressed block files (~57 GB) exist on the RAID. I could see them: ls /mnt/raid/garage/data/ dumped thousands of hash-named files. But without the LMDB mapping of (bucket, key) → block hash, blocks cannot be attributed to objects. A content-based classification (magic bytes → file extension) was considered but, at some point, you have to accept the loss and move on.
Lessons Learned
What went wrong
No metadata snapshots: Garage has provided
metadata_auto_snapshot_intervalsince v0.9.4. I hadn’t enabled it. This single config line would have made recovery trivial: stop Garage, swap in the snapshot, rungarage repair -a --yes tables, done. Honestly, with LMDB being the default engine and its corruption-on-power-loss behavior being documented, this option should be enabled by default.No Btrfs snapshots on the metadata partition: the RAID had snapper timeline snapshots, but the metadata partition on the NVMe didn’t. A Btrfs snapshot on the metadata directory would have been a second line of defense.
No replication: a single-node cluster with
replication_factor = 1means no peer to resync from. With just one additional node, I could have deleted the corrupt DB and recovered automatically.No monitoring: no alerting for Garage crash loops or LMDB assertion failures. I discovered the issue when my laptop was showing an empty folder instead of the mounted bucket. Then OpenCloud started returning 503s.
What went well
- Btrfs RAID1 preserved block data: all block data (~57 GB) was intact and transferable.
- restic backups outside S3: my critical data lives outside S3 and is backed by restic to a separate target. That pipeline was unaffected. The data I actually care about is safe.
- Snapper timelines on RAID:
/mnt/raid/subvolumes had Btrfs snapshot timelines (though the metadata wasn’t on RAID, so this didn’t help).
Preventive Measures Implemented
Garage metadata snapshots
Enabled Garage’s built-in snapshot mechanism. The snapshots are stored on the same RAID subvolume that already has snapper timeline snapshots, so they inherit Btrfs-level protection automatically:
# /etc/garage/config.toml
metadata_auto_snapshot_interval = "6h"
metadata_snapshots_dir = "/mnt/raid/garage/snapshots"
metadata_dir = "/var/lib/garage/meta"
data_dir = "/mnt/raid/garage/data"
Garage keeps the two most recent snapshots and deletes older ones automatically. Recovery from a snapshot is straightforward:
cd /var/lib/garage/meta
mv db.lmdb db.lmdb.bak
cp -r snapshots/2026-07-22T06:00:00Z db.lmdb
systemctl restart garage
garage repair -a --yes tables
Note: Garage docs point out that filesystem-level snapshots may be corrupted if taken during a write. Garage’s own snapshots are crash-consistent and should be your primary recovery path. Btrfs snapshots are a best-effort secondary line.
Future Recommendations
- Btrfs snapshots on the metadata partition: add snapper timeline snapshots on
/var/lib/garage/metaas a second layer of defense:
snapper -c garage-meta create-config /var/lib/garage/meta
- Scheduled metadata export:
garage meta dumpto a restic or S3 target:
#!/usr/bin/env bash
garage meta dump /tmp/garage-meta-dump.json
restic backup /tmp/garage-meta-dump.json
- Monitoring: alert on Garage process crashes and LMDB integrity. A simple systemd
OnFailurehook would catch crash loops. - Multi-node replication: even a second low-power node with
replication_factor = 2would eliminate the single point of failure. - Move metadata to RAID: migrating the metadata directory to the Btrfs RAID alongside block data would simplify backup and recovery to a single snapper configuration.
Roundup
LMDB corruption after power loss is a known issue. Garage ships with the tools to prevent this (metadata_auto_snapshot_interval, Btrfs/ZFS snapshot support, and multi-node replication), but none of them help if they aren’t configured. And I still think metadata_auto_snapshot_interval should default to something like "6h" when db_engine = "lmdb". It’s a footgun that costs users their data.
If you run Garage on LMDB, stop now and check:
- Is
metadata_auto_snapshot_intervalset? If not, add it to yourgarage.tomland restart. - Are the snapshots stored somewhere with redundancy? Set
metadata_snapshots_dirto a path on your RAID. - Do you have Btrfs/ZFS snapshots on the metadata directory? If you use snapper, add a config for it.
- Can you survive losing a single node? If not, add a second node and bump
replication_factor.
The cost of these precautions is near-zero. The cost of skipping them is an evening of recovery work and a permanent knot in your stomach when you see a thunderstorm approaching.
This is not pro-grade hardware in a datacenter with redundant power. It’s a home server plugged into a wall outlet. Failures are not just possible. They’re expected. If your data matters, act like it.