In the previous article , we explored NTFS—a filesystem where everything is a file, from your documents to the Master File Table itself. NTFS centralized all metadata into the MFT, using attribute-based records and a single journal to keep Windows volumes consistent and feature-rich.
Now let me introduce you to XFS, the filesystem designed for extreme scale. Originally built by Silicon Graphics in 1993 for their high-end IRIX workstations, XFS was engineered to handle filesystems measured in terabytes when most systems still counted in megabytes. Its core idea is to divide the disk into independent regions called Allocation Groups—each with its own free space tracking, its own inode management, and its own locks. This simple design choice is what allows XFS to scale linearly with the number of CPU cores and support filesystems up to 8 exabytes.
The other thing you’ll notice about XFS is its love for B+ trees. XFS uses them for almost everything—free space, inodes, file extents, reverse mappings, reference counts. This consistency makes XFS’s design elegant, even if the sheer amount of machinery under the hood is impressive.
Let’s see how all of this works, starting with the structure that makes it all possible.
The Physical Layout: Allocation Groups
As we mentioned earlier, XFS splits the disk into regions called Allocation Groups (AGs for short). A small filesystem might have just a few, while a very large one can have thousands. Let’s look at what that actually means on disk.
The first thing we’ll notice is that XFS has no boot sector. Most filesystems reserve the first sector for bootstrap code—the bit of machine code that starts the operating system. XFS skips that entirely. The partition starts immediately with AG 0, and AG 0 starts immediately with the superblock—the structure that holds the general filesystem metadata like block size, total number of blocks, the number of Allocation Groups, and other configuration data.

Each AG is a self-contained mini-filesystem. It starts with four metadata headers in sequential sectors, followed by all its data blocks:
- The superblock, which we already mentioned—block size, AG count, UUID, root directory inode, and other general configuration
- The AGF (AG Free space header), which keeps track of free space. It holds the roots of the free space B+ trees (BNO and CNT), as well as the reverse mapping and reference count trees
- The AGI (AG Inode header), which manages inodes. It holds the roots of the inode B+ trees (INO and FINO)
- The AGFL (AG Free List), a small reserve of blocks set aside for the AG’s own metadata to grow into
Don’t worry if those tree names don’t mean anything yet—we’ll go through each of them. The important thing is that all the B+ trees that manage an AG’s data are rooted in these headers. After the headers comes the bulk of the AG: the data blocks where actual file content and B+ tree nodes live. The metadata for AG 0 knows nothing about AG 3, and vice versa.

Why is this independence between AGs so important? Because it enables parallelism. Since each AG has its own metadata and its own locks, most operations in different AGs can proceed without blocking each other. Thread A can create a file in AG 0 while thread B allocates blocks in AG 1 at the exact same time—very little contention. There are still some shared resources—the journal and a few global counters—but the bulk of the work is per-AG, which is what lets XFS scale well across many CPU cores.
We’ve mentioned B+ trees several times now—they show up everywhere in XFS. Before we look at what each tree tracks, let’s understand how they actually work on disk.
B+ Trees: XFS’s Universal Tool
A B+ tree is a sorted, balanced tree where all the actual records live in the leaf nodes at the bottom, and the internal nodes above just contain keys and pointers to guide you down. Lookups are fast—even with millions of entries, you only need a few levels to reach the right leaf.
In XFS, each tree node occupies one filesystem block (typically 4 KB) from the AG’s data area. The AG header stores the block number of the root and the depth of the tree. XFS uses this same infrastructure for all its metadata—free space, inodes, reverse mapping, reference counting, extent maps, directories. The only thing that changes between them is what keys and records they store.
Now let’s see what each of these trees actually tracks, starting with free space.
Tracking Free Space: Two Trees Are Better Than One
We mentioned that the AG Free space header (AGF) points to the roots of two B+ trees. These two trees index the exact same free space data, just sorted differently. The BNO tree sorts free extents by block number. The CNT tree sorts them by size. Two trees for the same data costs a bit of extra overhead, but the reason becomes clear when you look at how XFS allocates blocks.
XFS uses three allocation strategies depending on what the caller needs. Exact is for when you need a specific block range—XFS just checks the BNO tree for that location. Near is the most common case, used when extending an existing file—XFS searches both trees in parallel, and the BNO tree usually wins quickly for small allocations since most nearby extents are big enough, while the CNT tree does better for larger allocations, especially on fragmented filesystems. Size is for when you don’t care about location, you just need the space—for example, a brand new file. XFS searches the CNT tree for the first extent that’s large enough.
When allocating in the AG where the file already lives, XFS uses the near strategy to keep data close together. When falling back to other AGs, it switches to size—locality across AGs matters less, so just find the space.
The AGF also stores summary information: the total number of free blocks and the longest free extent in this AG. This lets XFS quickly skip AGs that can’t satisfy a request without even touching the trees.
Free space is only half the story—XFS also needs to manage inodes, and it uses the same dual-tree trick.
Tracking Inodes: Another Pair of Trees
Each file or directory in XFS has an inode—a structure that stores its metadata (size, timestamps, permissions, and pointers to its data). The AG Inode header (AGI) points to the roots of another pair of B+ trees that manage them.
XFS allocates inodes in chunks of 64 inodes. The INO tree tracks all allocated inode chunks—where they live on disk and which inodes within each chunk are occupied. The FINO tree (Free INOde tree) is a subset: it only tracks chunks that have at least one free slot. This way, when you create a new file, XFS can jump straight to a chunk with space available instead of scanning all of them.
We know how XFS finds and allocates inodes. But what’s actually inside one?
Inside an Inode
Every file, directory, and symlink in XFS is represented by an inode. The inode stores the standard metadata you’d expect: the file size, timestamps (created, modified, accessed), ownership (user and group), and permissions.
Beyond that metadata, the inode has a flexible storage area that XFS splits into up to two sections, called forks:
- The data fork holds the main content associated with this inode. What that means depends on the type: for a regular file, it’s the map of which disk blocks hold the file’s bytes. For a directory, it’s the directory entries. For a symlink, it’s the target path.
- The attribute fork is optional and stores extra metadata that doesn’t fit in the standard fields—things like access control lists (ACLs) or custom labels that applications can attach to a file (what Linux calls extended attributes).
For directories and symlinks, if the data is small enough it can be stored right inside the inode itself—a short symlink or a directory with a handful of entries doesn’t need any extra disk reads. When the data grows beyond that, it spills out to disk blocks. Regular files, however, always use extents to point to data blocks on disk—their content is never inlined into the inode, even if the file is tiny.
Now let’s look at how directories and files use this structure.
Directories
A directory in XFS is really just a mapping from filenames to inode numbers. Each entry contains the filename, the inode number of the file or subdirectory it points to, and a file type tag (regular file, directory, symlink, etc.). The actual file metadata—size, timestamps, permissions—doesn’t live in the directory entry; it lives in the inode that the entry points to.
How these entries are stored in the data fork depends on how many there are. For very small directories—just a few entries—everything fits directly inside the inode. No extra blocks needed, no extra disk reads.
As the directory grows and can no longer fit in the inode, XFS moves to a block-based format where entries are stored in data blocks. For large directories, XFS builds a B+ tree indexed by a hash of the filename. Looking up a file in a directory with a million entries takes roughly 20 comparisons to navigate the tree, versus scanning potentially hundreds of thousands of entries linearly. This is a separate B+ tree implementation from the one used for free space and inodes—directory trees are hash-indexed and have different traversal needs—but the concept is the same.
Directories tell us where files are. Now let’s look at how files store their actual data on disk.
Files
For regular files, the data fork stores extents—each extent describes a contiguous range of blocks on disk: “starting at file offset X, this file’s data lives in blocks Y through Z.” Each extent record packs the file offset, the starting block number, the length of the extent, and a flag that marks whether the space is preallocated but not yet written to. All of this fits into just 128 bits.
For small files with just a few extents, these records fit directly inside the inode—one disk read gets you everything. When a file grows and has too many extents to fit in the inode, XFS moves them into a B+ tree stored in separate blocks on disk. The tree is indexed by file offset, so finding “which disk block stores byte 1,000,000 of this file?” is a quick tree traversal.
Now we know how XFS maps files to disk—but when does it decide where to put the data?
Delayed Allocation and Speculative Preallocation
XFS uses delayed allocation: when you write to a file, the data goes into memory and XFS reserves space in the free space counters, but it doesn’t pick specific disk blocks yet. The actual allocation happens later, when the kernel flushes data to disk. By that point XFS knows the full size of the write, so it can allocate one big contiguous extent instead of many scattered small ones. Less fragmentation, fewer metadata updates.
For files being written sequentially—log files, database imports, video recording—XFS goes a step further with speculative preallocation. It allocates extra space beyond the end of the file, starting small (64 KB) and doubling each time up to around 8 GB (the maximum extent size). If the file stops growing, a background scanner reclaims the unused space after 5 minutes.
All of these structures—free space trees, inode trees, extent maps, directories—are updated as files are created and modified. But what happens if the system crashes in the middle of an update?
Journaling: Write-Ahead Logging
XFS uses write-ahead logging to protect against crashes: before making any metadata change on disk, it writes a record of what it’s about to do into a journal. If the system crashes mid-operation, the journal is replayed on mount to finish or undo incomplete changes. XFS only journals metadata (not file data), which keeps the journal compact and fast.
XFS adds two optimizations on top of this. First, transactions don’t go to the journal immediately—they accumulate in an in-memory staging area called the Committed Item List (CIL). When the CIL reaches a size threshold, all the accumulated changes are flushed to the journal as a single checkpoint. This batching means fewer, larger writes instead of many small ones.
Second, once changes are in the journal they still need to reach their final locations on disk. The Active Item List (AIL) tracks this—a background thread works through it in order, pushing the oldest entries first. Only once the metadata is in its permanent location can the journal space be reclaimed.
The journal protects against crashes, but what about corruption that happens silently—a bad sector, a hardware glitch? XFS has an answer for that too.
Reverse Mapping: Who Owns This Block?
Most filesystems track ownership in one direction: given a file, you can find its blocks. XFS also tracks ownership in the reverse direction: given a physical block, you can find which file (or metadata structure) owns it.
This is done through the reverse mapping (RMAP) B+ tree, one per AG. Each entry records a range of physical blocks and who owns them—an inode number, or a special identifier for internal metadata like the journal or AG structures. This reverse index is what enables XFS to repair corrupted metadata while the filesystem is still online, and to report errors in terms of actual files rather than raw block numbers.
Reverse mapping also underpins XFS’s support for shared blocks, which brings us to reflink.
Reflink: Instant File Cloning
XFS supports reflink—copy-on-write file cloning. When you clone a file with cp --reflink=always, XFS points the new file’s extents at the same physical blocks as the original and increments a reference count. No data is duplicated; the copy is nearly instantaneous regardless of file size.
The reference counts are tracked in a reference count B+ tree, one per AG. When you later write to one of the cloned files, XFS detects that the affected blocks are shared (reference count greater than 1), allocates new blocks for the modified data, and decrements the count on the old blocks. The other file is completely unaffected.
We’ve now seen all the individual pieces. Let’s watch them work together.
Putting It All Together: Writing a File
Let’s trace what happens when you create and write a 1 MB file, to see how all these pieces work together.
First, XFS needs an inode. It picks an AG (based on the parent directory’s location) and consults the FINO tree to find a chunk with a free slot. It allocates the inode, adds a directory entry to the parent directory’s B+ tree, and logs the whole thing to the CIL.
Now you write 1 MB of data. The data goes into the page cache in memory, and XFS marks the extent as “delayed”—it reserves 256 blocks in the global free space counter, but doesn’t pick a specific AG or specific blocks yet. No disk allocation happens at this point.
When the kernel decides to flush the dirty pages, XFS’s allocator kicks in. It picks the preferred AG (where the file’s inode lives) and uses the near strategy—searching both BNO and CNT trees in parallel to find 256 contiguous blocks close to the file’s existing data. It allocates the blocks, updates both free space trees, records the ownership in the RMAP tree, and converts the delayed extent into a real extent in the file’s inode. All of this is logged to the CIL.
The CIL then aggregates this transaction with other recent transactions and writes a single checkpoint to the journal. Finally, the AIL’s background thread pushes the actual data and metadata to their permanent locations on disk. Once everything is written, the journal space can be reclaimed.
All those structures we’ve talked about—Allocation Groups, B+ trees, delayed allocation, the CIL, the AIL—work together to make this fast, contiguous, and crash-safe.
That’s a lot of machinery. Is it worth the complexity?
Tradeoffs
XFS became the filesystem of choice for enterprise and high-performance computing, and for good reason. It scales to truly massive filesystems and multi-core systems. Delayed allocation and speculative preallocation keep files contiguous. The journal is efficient thanks to CIL batching. RMAP enables self-repair. And all of this works while the filesystem stays online.
But there are tradeoffs. XFS is complex—around 200,000 lines of kernel code. Its in-memory structures use more memory than simpler filesystems. It has more overhead for very small files (under 4 KB) where the B+ tree machinery is overkill. And notably, XFS can grow but not easily shrink—there’s limited experimental support for shrinking, but in practice if you need a smaller partition you’ll likely need to back up, reformat, and restore.
For everyday desktop use, a simpler filesystem might be all you need. But for large filesystems, large files, and parallel workloads—think database servers, media storage, scientific computing—XFS is hard to beat.
Let’s wrap up with a quick recap of everything we’ve covered.
Summary
XFS divides the disk into independent Allocation Groups, each with its own B+ trees for free space, inodes, reverse mapping, and reference counting. Inodes use two forks—data and attributes—and files map their content to disk through extents. Delayed allocation and speculative preallocation keep files contiguous, while the CIL and AIL make journaling efficient. Reverse mapping and reflink round it out with block-level traceability and instant copy-on-write cloning.
The heart of it all is the Allocation Group architecture—by keeping each AG truly independent, XFS eliminates the global bottlenecks that limit most filesystems under parallel workloads. XFS is built for scale.
All the filesystems we’ve covered so far—FAT32, ext4, NTFS, and XFS—share one fundamental characteristic: they modify data in place. When you update a block, the new data overwrites the old data at the same disk location. In the next article , we’ll explore Btrfs—a filesystem that takes a radically different approach. Btrfs never modifies data in place. Every change creates a new copy at a new location. This “copy-on-write” design sounds wasteful, but it enables features that are impossible in traditional filesystems: instant snapshots, built-in RAID with self-healing, atomic transactions, and guaranteed data integrity.
Want to dive deeper? The XFS code lives in fs/xfs/ in the Linux kernel tree. Key files include xfs_alloc.c (allocation), xfs_bmap.c (extent mapping), xfs_btree.c (B+ trees), xfs_log.c (journaling), and libxfs/ (shared kernel/userspace code). Happy reading!
