ext4 & XFS
The two mainstream Linux filesystems — internals and trade-offs
Why These Two
On a modern Linux system, the default root filesystem is almost always ext4 (Debian, Ubuntu) or XFS (RHEL, Fedora Server, Oracle Linux, Rocky, Alma). Both are mature, journaled, extent-based, and production-proven on PB-scale deployments. They differ in design philosophy — and in a few operational properties that will bite you if you pick wrong.
ext4: Evolutionary Line
ext2
1993 — no journal, fast but fragile after crash
ext3
2001 — added journaling (JBD), otherwise same on-disk layout
ext4
2008 — extents, 48-bit block nrs, dir_index, delayed allocation, checksums
ext4 preserves forward and backward compatibility with ext2/ext3 for features not in use — you can mount an ext3 volume as ext4 and vice versa. This lineage is part of its appeal: battle-tested code with incremental, well-understood evolution.
ext4 On-Disk Layout
ext4 divides the block device into block groups, typically 128 MiB each. Each group holds everything needed to allocate files near their metadata — a locality strategy inherited from BSD's FFS.
Anatomy of a Block Group
| Region | Purpose |
|---|---|
| Superblock (copy) | Filesystem-wide metadata. Not in every group — only in groups 0, 1, and powers of 3, 5, 7 (sparse_super). |
| Group descriptor table | Points to this group's bitmaps and inode table. Duplicated wherever the superblock is. |
| Block bitmap | One bit per data block: free or in use. |
| Inode bitmap | One bit per inode slot: free or in use. |
| Inode table | Array of fixed-size inode structures (default 256 B each). |
| Data blocks | The rest — where file contents live. |
Note
Why backup superblocks matter: if block 0 gets corrupted, you can recover by pointing e2fsck at a backup: e2fsck -b 32768 /dev/sdX. mke2fs -n prints the locations without actually creating the FS.
ext4 Features Worth Knowing
| Feature | What it does |
|---|---|
extents | Replaces ext3's triple-indirect block tree with (offset, length, physical_block) triplets. Lower metadata overhead, faster for large files. |
dir_index | Hashed B-tree for large directories. Without it, directory lookup is linear — miserable past ~10k entries. |
flex_bg | "Flexible block groups": consolidates bitmaps and inode tables from several adjacent groups into one place, reducing seeks. |
huge_file | Uses i_blocks in cluster units instead of 512 B sectors — allows files > 2 TiB. |
uninit_bg | Marks unused block groups so fsck can skip them — drastically faster checks on large empty filesystems. |
inline_data | Small files (< ~60 B) stored entirely inside the inode's block-pointer area. No data block needed at all. |
64bit | 48-bit block numbers, enabling filesystems larger than 16 TiB (up to 1 EiB theoretical). |
metadata_csum | CRC32c checksums on superblock, group descriptors, bitmaps, inode table, extent tree, directory blocks. Detects silent corruption. |
encrypt | Per-directory encryption (fscrypt). Same key API used by Android and Chrome OS. |
ext4 Tuning
# # Create a filesystem tuned for a specific workload
# mkfs.ext4 -T largefile4 /dev/sdb1 # 1 inode per 4 MiB — huge media library
# mkfs.ext4 -T news /dev/sdc1 # 1 inode per 4 KiB — Maildir/news spool
# mkfs.ext4 -T largefile /dev/sdd1 # 1 inode per 1 MiB — sweet spot for bulk
#
# # Explicit inode count override
# mkfs.ext4 -N 5000000 /dev/sde1 # force 5M inodes regardless of size
#
# # RAID stripe alignment (stride = chunk/block, stripe-width = stride * data disks)
# mkfs.ext4 -E stride=16,stripe-width=64 /dev/md0
Reserved Blocks
By default, ext4 reserves 5% of the filesystem for root. This prevents unprivileged users from filling the disk and starving systemd-journald, syslog, or anything else that must keep writing. On a 10 TiB data volume, this is 500 GiB wasted — adjust with:
# tune2fs -m 1 /dev/sda1 # drop reserve to 1%
# tune2fs -m 0 /dev/sdb1 # no reserve (fine for data-only volumes)
Tip
Rule of thumb: keep the 5% reserve on the root FS. Drop it to 0 on pure-data mounts (backups, media, object stores, container image layers).
ext4 Command Line
| Command | Use |
|---|---|
mkfs.ext4 | Create a new ext4 filesystem. |
tune2fs | Change parameters on an existing FS: reserve %, mount count, label, UUID, feature flags. |
resize2fs | Grow (online) or shrink (offline) the filesystem. Grow is routine; shrink requires unmount and is slower. |
dumpe2fs | Print superblock and group info — fantastic for forensics. |
debugfs | Interactive low-level repair/inspection shell. Undelete files, inspect inodes by number, dump blocks. |
e2fsck / fsck.ext4 | Check and repair. |
# dumpe2fs -h /dev/sda1 | head -30
Filesystem volume name: root
Last mounted on: /
Filesystem UUID: 3f9a1b6e-...
Filesystem magic number: 0xEF53
Filesystem revision #: 1 (dynamic)
Filesystem features: has_journal ext_attr resize_inode dir_index
filetype needs_recovery extent 64bit flex_bg
metadata_csum_seed sparse_super large_file
huge_file uninit_bg dir_nlink extra_isize
encrypt
Inode count: 6553600
Block count: 26214400
Block size: 4096
Reserved block count: 1310720
...
XFS: Scalable From Day One
XFS was designed by Silicon Graphics in 1993 for IRIX — specifically to serve TB-sized video-editing workloads on multi-CPU workstations. Ported to Linux in 2001, merged upstream in 2002. Its entire design is oriented around scalability and concurrency: B+trees everywhere, parallel allocation groups, dynamic inode allocation.
RHEL made XFS the default root filesystem in RHEL 7 (2014), reflecting a quarter-century of production use.
XFS Design Principles
Allocation Groups (AGs)
An XFS filesystem is carved into a small number (usually 4–32) of allocation groups. Each AG is effectively a mini-filesystem with its own free-space and inode indexes. Because allocations within different AGs don't contend for the same locks, XFS scales to many concurrent writers far better than ext4.
B+ Trees Everywhere
- Free-space by block number — locate free runs adjacent to existing data.
- Free-space by length — find a run of exactly N free blocks.
- Inode B+tree — track which inode slots are allocated (inodes themselves are assigned dynamically, no pre-allocation).
- Directory B+tree — O(log n) lookups in directories with millions of entries.
- Extent B+tree inside the inode — maps logical offsets to physical extents for large files.
Dynamic Inode Allocation
XFS does not pre-allocate inodes at mkfs time. New inodes are carved out of free space on demand. Practical consequences:
- You can't run out of inodes while having free space — a common ext4 failure mode.
- Deleted inode space isn't automatically reclaimed to the pool of data blocks until the chunk is empty (this is usually invisible in practice).
Metadata-Only Journal
XFS journals metadata only. File data is never journaled. Ordering is enforced by writing data blocks before the metadata that references them, similar to ext4's data=ordered. There is no data=journal equivalent — if you need that level of durability, put the workload on a FS that supports it, or use application-level fsync.
XFS Features
| Feature | What it does |
|---|---|
| Delayed allocation | Writes stay in memory; physical blocks aren't chosen until flush. Lets the allocator see the full size of the write and place it contiguously. Excellent for reducing fragmentation. |
| Online defrag | xfs_fsr (filesystem reorganizer) runs while mounted and writable. Walks files, rewrites fragmented ones into contiguous extents. |
| Reflinks | Copy-on-write copies within a file or between files: cp --reflink=always a b shares extents until one side writes. Used heavily by container storage drivers and backup tools (borg, restic). |
| Quotas | User, group, and project quotas. Project quotas set limits on arbitrary directory trees — the mechanism Kubernetes uses for enforced per-volume quotas on XFS PVs. |
| Realtime subvolume | Separate data region with no metadata — extents allocated in fixed-size contiguous chunks. Deterministic I/O latency for video/audio workloads. Rare in practice. |
| CRC32c metadata checksums | On by default since mkfs.xfs 3.2.3 (2014). Every metadata block is verified on read. Silent corruption produces an explicit error, not mysterious misbehavior. |
| Scalability ceiling | 500 TiB per filesystem on 64-bit systems is routine; 8 EiB theoretical. Directories with tens of millions of entries remain fast. |
XFS Tuning
# # Default mkfs is already sane. Tune only when needed.
# mkfs.xfs /dev/sda1
#
# # RAID alignment — sunit in 512B sectors, swidth = sunit * data disks
# mkfs.xfs -d sunit=128,swidth=512 /dev/md0
#
# # More allocation groups for a high-concurrency workload
# mkfs.xfs -d agcount=32 /dev/sdb1
#
# # Larger log buffer — reduces log commit frequency under heavy metadata load
# mount -o logbsize=256k /dev/sda1 /data
#
# # Inspect an existing XFS
# xfs_info /data
meta-data=/dev/sda1 isize=512 agcount=4, agsize=6553600 blks
= sectsz=4096 attr=2, projid32bit=1
= crc=1 finobt=1, sparse=1, rmapbt=0
data = bsize=4096 blocks=26214400, imaxpct=25
= sunit=0 swidth=0 blks
naming =version 2 bsize=4096 ascii-ci=0, ftype=1
log =internal log bsize=4096 blocks=12800, version=2
= sectsz=4096 sunit=1 blks, lazy-count=1
realtime =none extsz=4096 blocks=0, rtextents=0
XFS Utilities Cheat Sheet
mkfs.xfs— createxfs_info— likedumpe2fs -h, but for XFSxfs_admin— change label / UUIDxfs_growfs— online grow (no offline option)xfs_fsr— online defragmenterxfs_repair— offline checker/repair (XFS has nofsck.xfsthat does real work; the mount-time log replay handles most cases)xfs_db— low-level debugger, equivalent todebugfsxfs_quota— manage user/group/project quotas
Growing and Shrinking
Grow: online, with resize2fs /dev/sda1 after the underlying block device/LV has been enlarged.
Shrink: offline only — unmount, e2fsck -f, resize2fs <dev> <size>, then shrink the block device. Slow but supported.
Grow: online, with xfs_growfs /mount/point.
Shrink: NOT SUPPORTED. Ever. There is no tool. The only way to "shrink" an XFS volume is xfs_dump/xfsrestore to a smaller one, or rsync + reformat.
Warning
The XFS shrink gotcha: If you provision an XFS volume too large — say, a 10 TiB LV on a thin-provisioned pool — you're committed. There's no going back without downtime and data migration. Always err on the side of smaller XFS volumes and grow later. On LVM, plan capacity carefully; on cloud (EBS, GCE PD), remember that volume shrink isn't supported at the block layer anyway, so this is often moot.
ext4 vs XFS — Side by Side
| Property | ext4 | XFS |
|---|---|---|
| Max filesystem size (practical) | ~50 TiB recommended | PB scale, tested to 500+ TiB |
| Max file size | 16 TiB | 8 EiB |
| Inodes | Pre-allocated at mkfs (can run out) | Dynamic (cannot run out independently) |
| Journaling | Metadata + optional data journaling | Metadata only |
| Allocation | Block bitmap + extent tree | B+ trees (free by block, free by length) |
| Concurrency | Good; serialized on group bitmap locks | Excellent; independent AGs |
| Delayed allocation | Yes (since 2.6.30) | Yes (original) |
| Reflinks (CoW) | No | Yes |
| Project quotas | Experimental / limited | First-class (used by K8s) |
| Metadata checksums | Optional (default on mkfs ≥ 1.43) | On by default |
| Shrink | Offline, supported | Not supported |
| Online grow | Yes (resize2fs) | Yes (xfs_growfs) |
| Small-file performance | Strong, especially with inline_data | Good; large inode size helps |
| Large-file throughput | Good | Excellent — the original design target |
Choosing Between Them
- It's a general-purpose volume < 16 TiB.
- You may need to shrink later.
- Workload is small files and you want
inline_data. - You want the absolute maximum ecosystem maturity on Debian/Ubuntu.
- You need
data=journalfor a paranoid durability requirement.
- The filesystem is > 16 TiB, or will grow large.
- Many concurrent writers (database storage, mail spools, object-store backends).
- Workload is large files (video, backups, databases).
- You need reflinks (container storage, dedup-aware backups).
- You need project quotas (Kubernetes XFS PVs).
- You're on RHEL-family and want the distro default.
Tip
Default heuristic: ext4 for the root filesystem and small data volumes, XFS for anything that will get large or concurrent. If you have no opinion, follow your distro's default.
Honorable Mentions
| FS | Status | Notes |
|---|---|---|
f2fs | Mainline, production on Android | Flash-Friendly FS. Log-structured design tailored for NAND erase-block behavior. Solid choice for low-end SSDs, SD cards, embedded devices. Rarely used for general-purpose servers. |
ReiserFS / Reiser4 | Deprecated | Innovative B+tree design in the early 2000s, later abandoned. Removed from mainline Linux in 6.13. Not a choice for new deployments. |
bcachefs | Experimental (merged in 6.7, 2024) | CoW filesystem with built-in tiered caching, compression, snapshots, encryption. Ambitious — aims to be what btrfs was supposed to be. Not yet production-grade. Worth watching. |
Note
btrfs and ZFS are covered separately under Advanced Filesystems (storage-08). They're CoW filesystems with volume management, snapshots, and checksumming baked in — a fundamentally different architecture from ext4/XFS.
Takeaways
- ext4 and XFS are the two production defaults on Linux. Both are journaled, extent-based, and battle-tested.
- ext4 evolved from ext2/ext3 with backward compatibility; XFS was built scalable from day one.
- ext4 pre-allocates inodes (risk of
ENOSPCwith free space); XFS allocates them dynamically. - XFS journals metadata only. No
data=journalequivalent. - XFS cannot shrink. Plan capacity conservatively or accept migration downtime.
- Use reflinks (
cp --reflink) on XFS for fast CoW copies — huge win for backups and container layers. - Default heuristic: ext4 for small/shrinkable volumes, XFS for large/concurrent/large-file workloads.
- f2fs for flash-only embedded; bcachefs still baking; ReiserFS is history.