Btrfs Filesystem Internals

Btrfs (the “B-tree filesystem”, pronounced variously “butter-eff-ess”, “better-eff-ess”, or “b-tree-eff-ess”) is a copy-on-write (CoW) filesystem for Linux built almost entirely out of one data structure used many times: a B-tree. Where a traditional filesystem hardcodes inode tables, bitmaps, and directory formats as distinct on-disk structures, Btrfs stores everything — inodes, directory entries, file-extent maps, free-space tracking, data checksums, device geometry, subvolume roots — as variable-length items keyed by a (objectid, type, offset) triple inside a small forest of B-trees, with one master tree (the tree of tree roots) pointing at all the others (per the Btrfs design doc and trees overview). Because it never overwrites live blocks, Btrfs can offer writable snapshots, reflinks, per-block data checksums, transparent compression, and integrated multi-device RAID — at the cost of fragmentation and metadata overhead. This note covers the architecture and on-disk structure; the CoW transaction model and the subvolume/snapshot user-facing mechanism are the subject of its sibling Btrfs Copy-on-Write Subvolumes and Snapshots.

Version & status

Facts here are pinned to Linux 6.12 LTS (released 2024-11-17) unless a feature note gives an explicit “since” version. The Btrfs on-disk format is declared stable (it will not change without very strong reason, and older formats stay mountable by newer kernels — per the Status page). The one prominent unstable feature is the RAID5/RAID6 profile, covered in the multi-device section below; do not use it in production as of 6.12.


Mental Model: a Forest of Copy-on-Write B-trees

The single most useful idea is that Btrfs is a “B-tree of B-trees.” A small fixed set of B-trees each store one category of metadata, and a master tree — the tree of tree roots (objectid BTRFS_ROOT_TREE_OBJECTID = 1) — holds the root block pointers and names for all the others, including every subvolume and snapshot (trees doc; confirmed in btrfs_tree.h). The superblock — a fixed-location structure replicated at several offsets on each device — points at the root tree and the chunk tree, and from those two everything else is reachable.

The B-tree design itself comes from Ohad Rodeh’s 2006 work on “B-trees, shadowing, and clones” (IBM Research), which the design doc cites as the theoretical basis; the academic write-up of the whole system is “BTRFS: The Linux B-Tree Filesystem”, ACM Transactions on Storage, August 2013 (design doc references). “Shadowing” is precisely the CoW-the-tree-upward technique that gives Btrfs atomic, consistent updates — see Btrfs Copy-on-Write Subvolumes and Snapshots for the full transaction walk-through.

flowchart TB
  SB["Superblock<br/>(fixed offsets, replicated per device)"]
  SB --> RT["Tree of tree roots<br/>(objectid 1)<br/>names + root pointers for all trees"]
  SB --> CT["Chunk tree<br/>(objectid 3)<br/>logical to physical mapping"]
  RT --> ET["Extent tree (2)<br/>refcounts + back-references"]
  RT --> DT["Device tree (4)<br/>physical allocation per device"]
  RT --> CS["Checksum tree (7)<br/>per-4KiB data csums"]
  RT --> FS1["FS tree (5)<br/>default subvolume: inodes, dirents, file extents"]
  RT --> FS2["FS tree<br/>another subvolume / snapshot"]
  RT --> FST["Free-space tree (10)<br/>space_cache=v2"]
  RT --> UT["UUID tree (9)"]
  RT --> BG["Block-group tree (11)<br/>fast mount"]
  FS1 -->|"file extent item"| ET
  FS2 -->|"file extent item"| ET
  CT --> DEV["Physical devices"]
  DT --> DEV

The Btrfs B-tree forest. What it shows: the superblock anchors two trees (root tree and chunk tree); the root tree is the directory of every other tree, including one FS tree per subvolume/snapshot; FS trees point into the shared extent tree, which is the allocation map and reference-count ledger. The insight: there is no special “inode table” or “directory file” format — every structure is items in a B-tree, so the same lookup, insert, and CoW code services all metadata. Two FS trees pointing at the same extent (e.g. FS1 and FS2) is exactly how snapshots share data for free.


The Key: (objectid, type, offset)

Every item in every Btrfs B-tree is addressed by a 17-byte key. From the v6.12 UAPI header (btrfs_tree.h):

struct btrfs_disk_key {        /* on-disk, little-endian */
    __le64 objectid;           /* which object this item belongs to */
    __u8   type;               /* what kind of item it is */
    __le64 offset;             /* type-dependent: file byte offset, csum byte, devid... */
} __attribute__ ((__packed__));
 
struct btrfs_key {             /* in-memory, native byte order — otherwise identical */
    __u64  objectid;
    __u8   type;
    __u64  offset;
} __attribute__ ((__packed__));

The three fields are compared in order — objectid first, then type, then offset — which means all items belonging to one filesystem object cluster together in key order. The objectid is the most significant component: for a file or directory it is the inode number, and “allocating an inode number” is literally finding a hole in the key space (an objectid not yet present — design doc). The type distinguishes the dozens of item kinds; from the header, the common ones are BTRFS_INODE_ITEM_KEY = 1 (stat data), BTRFS_INODE_REF_KEY = 12 (this inode’s name and parent dir), BTRFS_XATTR_ITEM_KEY = 24, BTRFS_DIR_ITEM_KEY = 84 and BTRFS_DIR_INDEX_KEY = 96 (the two directory indexes), BTRFS_EXTENT_DATA_KEY = 108 (a file’s data map), BTRFS_EXTENT_CSUM_KEY = 128 (data checksums), and BTRFS_ROOT_ITEM_KEY = 132 (a pointer to a tree root). The offset is overloaded per type: for a file-extent item it is the byte offset within the file; for a checksum item it is the on-disk byte number of the data being summed; for a device item it is the device id.

Because keys sort objectid → type → offset, a directory’s entries, a file’s extents, and a file’s checksums are each a contiguous key range — a single B-tree range scan retrieves “everything about object N” or “all extents of file N from offset X.”

Nodes, leaves, and the block header

Btrfs B-tree blocks come in two shapes. Internal nodes hold only [key, block pointer] pairs (struct btrfs_key_ptr, which also stores the generation the pointed-to block should have). Leaves hold the actual items, laid out as two arrays growing toward each other: a fixed-size struct btrfs_item array at the front (each item = key + offset + size locating its data) and the variable-length item data growing backward from the end (design doc):

struct btrfs_item {
    struct btrfs_disk_key key;
    __le32 offset;             /* where item data lives in the leaf */
    __le32 size;               /* how big that data is */
} __attribute__ ((__packed__));
/* Leaf layout: [item0, item1, ... itemN] [free space] [dataN ... data1, data0] */

Keeping keys packed at the front and data at the back keeps the keys cache-dense for binary search (design doc). Every block — node or leaf — begins with a struct btrfs_header carrying a checksum of the block, the filesystem UUID, the block’s own logical address (bytenr), a generation number (the transaction id that wrote it), the owner tree, item count, and level. BTRFS_MAX_LEVEL is 8, so the tree is at most 8 levels deep.

The generation field is doing subtle, important work: every pointer to a block also records the generation it expects that block to have. On read, if the block’s stored generation does not match what the parent expected, Btrfs has caught a phantom or misplaced write — a write the drive claimed to do but didn’t, or did to the wrong place — without having to re-checksum and rewrite the parent every time (design doc). The block’s checksum is not stored in the parent pointer (only the generation is), deliberately, to keep writeback simple: the generation is known when the block is linked in, but the checksum is computed only just before the block hits disk.


The Trees of the Forest

The forest is a small, fixed cast of trees, each owning one concern (trees doc; objectids from btrfs_tree.h):

  • Tree of tree roots (BTRFS_ROOT_TREE_OBJECTID = 1) — the master directory. Holds ROOT_ITEM pointers to every other tree and to every subvolume/snapshot FS tree, plus directory items mapping subvolume names to those roots, and the deletion-progress record used when cleaning a deleted subvolume.
  • Chunk tree (= 3) — the logical-to-physical map. Btrfs addresses all metadata and data in a single 64-bit logical address space; the chunk tree translates a logical range into one or more physical (device id, physical offset) extents. CHUNK_ITEMs map a virtual chunk to physical storage and encode the RAID profile of that chunk; DEV_ITEMs describe each underlying block device. To bootstrap (you cannot read the chunk tree until you can map its blocks), the superblock duplicates the chunk items needed to find the chunk tree itself (trees doc).
  • Extent tree (= 2) — the allocation map and the reference-count ledger; covered in its own section below. It is the heart of how snapshots are cheap.
  • Device allocation tree (= 4) — records which physical regions of each device have been carved into chunks; small, updated only on chunk allocation, with back-references to the chunk that owns each physical extent.
  • Checksum tree (= 7) — every 4 KiB data block gets a detached checksum stored here, keyed by the block’s on-disk byte number; the value is a run of checksums for consecutive blocks.
  • FS tree(s) (= 5 is the top-level subvolume; others allocated dynamically) — one per subvolume or snapshot, storing that subvolume’s inodes, directory entries, and file-extent maps. Keys here use the inode number as objectid.
  • Free-space tree (= 10) — the B-tree-based free-space tracking of space_cache=v2, the successor to the old inode-backed v1 free-space cache.
  • UUID tree (= 9) — UUID-to-subvolume mapping, used for fast lookup during send.
  • Quota tree (= 8) — qgroup status and relations; exists only when quotas are enabled.
  • Block-group tree (= 11) — an optional feature that gathers all block-group items into one tree so mount does not have to seek all over the (potentially huge) extent tree to find them; cures slow mounts on large filesystems.
  • Log tree (BTRFS_TREE_LOG_OBJECTID) — a separate write-ahead log used only to speed up fsync. This is not the source of crash consistency (CoW provides that); it is a per-transaction durability optimization, replayed on the next mount. The distinction matters and is drawn carefully in Btrfs Copy-on-Write Subvolumes and Snapshots and contrasted with classic journaling in Crash Consistency and fsck.
  • Raid-stripe tree (= 12) — newer, separate tracking of physical stripe placement that decouples physical from logical offsets; used by zoned mode and (prospectively) a reworked RAID56.

File Data: Extents and the file_extent_item

A file’s data is described by EXTENT_DATA items (btrfs_file_extent_item) in its FS tree. Small files can be inlined directly into the leaf (BTRFS_FILE_EXTENT_INLINE = 0) if they fit within one node block; the design doc notes the inline data is covered by the leaf block’s header checksum rather than a separate csum item (design doc). Larger files use regular extents. From the v6.12 header:

struct btrfs_file_extent_item {
    __le64 generation;       /* transaction that created this extent */
    __le64 ram_bytes;        /* uncompressed in-RAM size (upper bound) */
    __u8   compression;      /* none / zlib / lzo / zstd */
    __u8   encryption;       /* reserved */
    __le16 other_encoding;
    __u8   type;             /* INLINE / REG / PREALLOC */
    __le64 disk_bytenr;      /* logical address of the extent on disk */
    __le64 disk_num_bytes;   /* bytes the extent occupies on disk */
    __le64 offset;           /* offset INTO that on-disk extent this item starts at */
    __le64 num_bytes;        /* logical (uncompressed) length this item maps */
} __attribute__ ((__packed__));

The disk_bytenr / disk_num_bytes pair names the physical extent; the crucial subtlety is offset and num_bytes, which let one file-extent item reference the middle of a larger on-disk extent. This is what makes a partial overwrite cheap: writing 1 MiB into the middle of an existing 128 MiB extent yields three extent items — [old 0–64 MiB], [new 1 MiB], [old 65–128 MiB] — pointing into the same and a new on-disk extent, without rereading the old data (design doc). The same mechanism — two extent items in two different files pointing into one on-disk extent — is exactly how reflinks and snapshot data-sharing work (see Btrfs Copy-on-Write Subvolumes and Snapshots). The PREALLOC type marks space reserved by fallocate() that has no data yet.

Directories are indexed twice: a DIR_ITEM index keyed by the crc32c hash of the filename (for name → inode lookup) and a DIR_INDEX index keyed by a per-directory sequence number (for readdir to return entries in roughly on-disk order, which makes bulk reads like backups fast) (design doc).


The Extent Tree and Explicit Back-References

The extent tree is the linchpin that makes snapshots and reflinks possible. For every extent allocated to a B-tree block or a file, it stores a btrfs_extent_item recording the number of references to that extent, and it doubles as the in-use map for the whole device (design doc). When two subvolumes (or a snapshot and its origin, or two reflinked files) point at the same extent, its refcount is >1; nobody may free it until the count drops to zero. Reference counting is the basis of the entire snapshotting subsystem — share a tree root, bump the refcount, and two subvolumes diverge only as they are written, each new write decrementing/incrementing counts as blocks are replaced.

Btrfs keeps not just counts but explicit back-references: from an extent, you can find who points at it. These exist for three reasons (design doc): (1) to validate that a reference being dropped was genuinely held before freeing the extent; (2) to answer “if this block is reported corrupt, which file(s) and subvolume(s) does it belong to?” — turning a scrub error into a list of affected files; and (3) to migrate blocks when shrinking the filesystem or rebalancing the storage pool, which needs to find and update every pointer to a moving extent. A given file extent can be referenced by multiple snapshots/subvolumes, by different files in one subvolume, or by different offsets within one file — and the back-reference machinery enumerates all of them.

Extents are grouped into block groups (chunks of 256 MiB, 1 GiB, or more) flagged for data, metadata, or system use; metadata block groups cluster small B-tree blocks, data block groups hold file content. Separating them lets the allocator and the RAID profiles treat the two differently (you can mirror metadata while striping data).


Integrated Multi-Device and RAID Profiles

Btrfs has volume management built in — it can span multiple block devices natively, no LVM or md underneath (volume management). Each block-group profile is an allocation policy describing redundancy as a function of device count, applied independently to data and metadata. The supported profiles include single, dup (two copies on one device), RAID0 (striping), RAID1 / RAID1c3 / RAID1c4 (2/3/4 mirrored copies), RAID10, and RAID5 / RAID6. Because profiles are per-block-group and per-data/metadata, you can run, e.g., RAID1 metadata over single data, and convert a live filesystem from one profile to another with btrfs balance ... -dconvert=/-mconvert=, which rewrites and re-points every affected block group (volume management).

RAID5/RAID6 is unstable — do not use in production

Per the Status page and btrfs(5) “RAID56 status and recommended practices”, as of 6.12 the RAID5/6 profiles have design and implementation deficiencies and “should not be used in production, only for evaluation or testing.” The key defect is the classic write hole: an unclean shutdown can leave a stripe where some data ranges and the parity are from old writes and some from new, with no record of which is which — and no write journal is implemented to close it. (A full read-modify-write of every stripe would avoid the hole but was too slow to ship.) The recommendation is concrete: never use raid5/raid6 for metadata — use raid1 (or raid1c3 to tolerate two device losses) instead, because rebuilding a mirror only needs the surviving copy whereas a striped profile needs every device. Power-failure safety for metadata under RAID56 “is not 100%.” Verify the current status note when reading; this is the most fast-moving correctness caveat in Btrfs. uncertain

When a read finds damage and a redundant copy exists (dup, RAID1-like, or RAID5/6), Btrfs performs auto-repair on read: it returns the good copy to the application and overwrites the bad copy in place, logging a message (auto-repair). But this only fixes copies that are actually read; to verify and repair all copies you must run scrub (btrfs scrub), which walks every block, verifies its checksum, and repairs from redundancy. This self-healing is only possible because of the per-block checksums described next.


Checksums

Data and metadata are checksummed by default (checksumming). A metadata block’s checksum is inline in its btrfs_header; each 4 KiB data block has a detached checksum stored in the checksum tree. The checksum is computed before writing and verified after reading — the combination of “verify on read” plus “another good copy exists” is what enables auto-repair and scrub above.

Several algorithms are available; from the v6.12 header (enum btrfs_csum_type):

BTRFS_CSUM_TYPE_CRC32  = 0,   /* crc32c — default, 32-bit */
BTRFS_CSUM_TYPE_XXHASH = 1,   /* xxhash64 — 64-bit */
BTRFS_CSUM_TYPE_SHA256 = 2,   /* SHA-256 — 256-bit, cryptographic */
BTRFS_CSUM_TYPE_BLAKE2 = 3,   /* BLAKE2b-256 — 256-bit, cryptographic */

The default and most backward-compatible is crc32c — very fast (modern CPUs have a hardware instruction) with good error detection but no collision resistance. The other three were added in kernel 5.5: xxhash (64-bit, a fast crc32c successor with better collision resistance), and the two cryptographic-strength 256-bit hashes SHA-256 (slow but FIPS-certified) and BLAKE2b (faster than SHA-256, SIMD-accelerable). The csum type is chosen at mkfs time and cannot change on a mounted filesystem (checksumming). The digest occupies a fixed 32-byte area in metadata blocks regardless of algorithm, so cryptographic hashes cost no extra metadata space there; only the per-data-block checksums in the csum tree grow with digest size.

Uncertain

Verify: that crc32c remains the mkfs.btrfs default at the btrfs-progs version shipping with 6.12-era distros (the algorithm is chosen by userspace mkfs, not the kernel). Reason: the kernel header only enumerates supported types; it does not encode the userspace default, which the docs state is crc32c but could be re-defaulted in newer btrfs-progs. To resolve: check mkfs.btrfs(8) for the exact btrfs-progs release. uncertain


Compression

Btrfs supports transparent per-extent compression with three algorithms: ZLIB (slower, higher ratio; levels 1–9, default 3), LZO (fastest, no levels, worse ratio), and ZSTD (added in v4.14; comparable ratio to ZLIB at higher speed; levels 1–15 since v5.1) (compression). Data is split into 128 KiB chunks before compression so random rewrites do not force decompressing a whole large extent; the extra extents raise metadata consumption. Compression is selected by mount option (-o compress=zstd:3), by the per-file property (btrfs property set file compression zstd), or by the legacy chattr +c. Compression requires CoW and data checksumsnodatacow or nodatasum disables it (compression). A pre-compression heuristic (entropy, byte-frequency, repeated-pattern tests) skips data unlikely to compress, and incompressible files get a sticky NOCOMPRESS flag.

Uncertain

Verify: the exact set of ZSTD levels at 6.12. The latest docs state negative levels −15..−1 since v6.15after 6.12 — so on a 6.12 kernel the ZSTD range is 1..15 only. Reason: the docs describe the latest tree; the −15..−1 range postdates 6.12. To resolve: confirm against the 6.12 fs/btrfs/zstd.c level handling. uncertain


Send / Receive — Streamable Differential Backup

btrfs send traverses a read-only subvolume and emits a stream of encoded commands that recreate it (full mode) or that express the difference from one or more reference subvolumes (incremental mode); btrfs receive replays the stream to reconstruct an equivalent subvolume on another filesystem (send/receive). The stream commands manipulate metadata (owner, permissions, xattrs), data extents (create, clone, truncate), and whole-file operations (rename, delete); each command is CRC32C-protected. Incremental send is the backbone of efficient Btrfs backups: because read-only snapshots fix a point-in-time tree (see Btrfs Copy-on-Write Subvolumes and Snapshots), the sender can compute exactly which extents changed by comparing generation numbers, and transmit only those. A received subvolume is read-only and carries a received_uuid linking it to its source, which the next incremental send relies on.


Failure Modes and Common Misunderstandings

  • “A snapshot is a backup.” It is not. A snapshot shares data blocks with its origin via CoW; if a block is physically damaged (bad sector, bad RAM, dd accident), both the snapshot and the origin see the damage (subvolumes doc). Snapshots protect against logical mistakes (accidental delete, bad upgrade), not media failure — for that you need redundancy (RAID/dup) and off-device backups (send/receive).
  • CoW fragmentation under random rewrite. Because overwrites never happen in place, databases and VM images with heavy random writes fragment badly, hurting sequential read throughput. Mitigations: autodefrag, the nodatacow attribute (chattr +C), or nodatacow mounts — but nodatacow also turns off checksums and compression for those files. This tradeoff is detailed in Btrfs Copy-on-Write Subvolumes and Snapshots.
  • ENOSPC on a “non-full” filesystem. Because data and metadata live in separately-profiled block groups, a filesystem can report free space while metadata block groups are exhausted, causing ENOSPC on operations that need metadata. btrfs balance reclaims partially-used block groups; this is a frequent operational surprise.
  • Direct I/O vs data checksums. A data checksum is computed just before submitting to the device, so the buffer must not change until writeback finishes. O_DIRECT can let userspace mutate the buffer mid-flight, causing a false checksum mismatch; kernels since 6.14 force such direct writes to fall back to buffered when the inode requires a data checksum (checksumming). On 6.12 this fallback is not present.
  • RAID56 metadata. Covered above — using raid5/raid6 for metadata is the most common dangerous misconfiguration.

Alternatives and When to Choose Them

  • vs ext4 / XFS (journaling): ext4 and XFS overwrite data in place and protect consistency with a journal (see The jbd2 Journaling Layer); they have lower write amplification and are battle-tested, but offer no per-block data checksums, no cheap snapshots, and no built-in multi-device redundancy. Choose them for databases/VMs with heavy random rewrite, or when predictability beats features.
  • vs ZFS: the closest peer — also CoW, also with checksums, snapshots, and integrated RAID (RAID-Z). ZFS is generally regarded as more mature for multi-device pools but is out-of-tree on Linux for licensing reasons; Btrfs is in-tree and ships as a default option on several distributions (see Production Notes).

Uncertain

Verify: that ZFS RAID-Z closes the write hole that Btrfs RAID56 leaves open. Reason: this is the widely-stated rationale for preferring ZFS for parity RAID, but it is not sourced from a primary doc fetched here. To resolve: check the OpenZFS documentation on RAID-Z and the variable-stripe-width design. uncertain

  • vs bcachefs: the newest in-tree CoW contender (mainlined 6.7), aiming at Btrfs-like features with a tiering/caching focus; verify its stability at read time.
  • vs F2FS: log-structured, tuned for flash; Btrfs’s zoned mode overlaps but F2FS targets mobile/embedded flash specifically.

Choose Btrfs when you want writable snapshots, send/receive backups, end-to-end data checksums with self-healing, transparent compression, and flexible multi-device mirroring — and can tolerate CoW fragmentation and metadata overhead.


Production Notes

Btrfs is the default root filesystem on Fedora and openSUSE, where it underpins automatic pre/post-update snapshots (snapper) and one-step rollbacks — the canonical “system root layout” puts /var, /var/log, etc. into separate subvolumes so they are not rolled back with the system root (subvolumes doc). The back-reference mechanism’s reference description comes from a Josef Bacik (a longtime Btrfs developer at Meta/Facebook) email that the design doc links to as authoritative (design doc), reflecting Meta’s significant role in Btrfs development. The two most common operational scars are (1) the RAID5/6 write hole — teams use raid1/raid1c3 instead — and (2) ENOSPC from unbalanced block groups, managed with scheduled btrfs balance.

Uncertain

Verify: the exact Fedora/openSUSE release in which Btrfs became the default root filesystem (commonly cited as Fedora 33, 2020). Reason: distro-default version not confirmed against a primary release announcement here. To resolve: check the Fedora 33 release notes / change proposal. uncertain Scrub-on-a-schedule plus redundant profiles is the standard way to actually realize the self-healing the checksums promise; without periodic scrub, latent corruption in unread copies goes undetected.


See Also