Inodes
On Unix-style file systems the inode is the file: a fixed-size record holding mode, owner, size, timestamps, link count and the block map — everything except the name, which lives in directories that point at it by number.
The problem
df -h says 40% free. touch x says "No space left on device". Every engineer meets this once, usually at 3 a.m., and the explanation is that a file system has two things that can run out — and only one of them is bytes.From a name to an inode to blocks
A directory on ext4, XFS, btrfs or APFS is a list of (name, inode number) pairs — a hash-indexed tree on ext4 (dir_index), a B+tree on XFS. The name resolves to a number; the number indexes the file system’s inode table (ext4) or inode B-tree (XFS, btrfs). The inode is the record found there. It is the file; the name is a pointer to it, and there may be several such pointers or, transiently, none (Files, Paths and Names).
What the inode holds: mode (type — regular, directory, symlink, device, socket, FIFO — and the permission bits), uid/gid, size in bytes, timestamps (atime last access, mtime last content change, ctime last inode change; ext4 also has a creation time visible via statx), link count, the number of blocks allocated, and the block map. What it does not hold: its name, and its path. stat prints all of it; ls -i prints just the number.
The inode number is unique within one file system, not across the machine. Two files on different mounts can share number 12; the pair (device, inode) is the real identity, which is what rsync, tar and backup tools use to recognise hard links and what find -samefile compares. On XFS the inode number encodes its location (allocation group, block, offset), which is why XFS numbers are large and sparse.
$ ln app.log app.log.link $ ls -i app.log app.log.link 918273 app.log 918273 app.log.link $ stat app.log File: app.log Size: 84213487616 Blocks: 164479472 IO Block: 4096 regular file Device: 259,2 Inode: 918273 Links: 2 Access: (0644/-rw-r--r--) Uid: ( 1001/ app) Gid: ( 1001/ app) Access: 2026-08-25 09:12:01.104 +0000 Modify: 2026-08-25 09:11:59.980 +0000 Change: 2026-08-25 09:12:00.002 +0000 Birth: 2026-08-16 02:00:11.000 +0000
Direct and indirect pointers vs extents
The classic Unix block map, still used by ext2 and ext3, is an array inside the inode: 12 direct pointers to data blocks, then one pointer to a single-indirect block (itself a block full of 1024 pointers with 4 KiB blocks and 4-byte addresses), one to a double-indirect block (1024 × 1024), and one to a triple-indirect block. Small files — the vast majority — need only the direct pointers; the first 48 KiB is reachable with zero extra reads, the next 4 MiB with one, and so on up to about 4 TiB. Reading byte 3 GB of a file means following three levels of pointers, each a potential disk read.
The cost is that a contiguous 1 GB file still needs 262,144 individual pointers spread across indirect blocks, all of which must be read to map it, and all of which must be written to free it (deleting a large file on ext3 was famously slow). Extents replace the pointer array with (logical block, physical block, length) triples: one entry can describe up to 128 MiB of contiguous space on ext4. The inode holds four extents inline; larger or fragmented files spill into an extent tree, a B+tree whose leaves are extents. XFS, btrfs, APFS and NTFS all use extent-shaped maps in their own tree structures.
Extents are why fragmentation matters again in a way it did not on a fresh pointer-based file system: a file appended in tiny increments while other files are also growing ends up with thousands of short extents, and its read pattern becomes random rather than sequential. filefrag -v shows the map; e4defrag and XFS’s xfs_fsr rewrite files contiguously. Preallocation (fallocate) is how databases and log writers reserve a contiguous run up front.
- Pointer-based map: constant inode size, depth grows with file size, one extra read per level.
- Extent-based map: one entry per contiguous run; sequential files are tiny to describe; fragmented files grow a tree.
Blocks:instatis the allocated count in 512-byte units; a sparse file has a size far larger than its blocks.
Hard links, link count and what rm does
The link count in the inode is the number of directory entries that point at it. ln adds an entry and increments it; rm — unlink(2) — removes an entry and decrements it. Nothing about the data changes until the count hits zero and the last open descriptor closes; only then are the blocks returned to the free bitmap and the inode itself freed. A directory’s count starts at 2 (its own entry in the parent, plus its own .) and grows by one for every subdirectory’s .., which is why ls -ld on a directory with 5 subdirectories shows 7.
Hard links cannot cross file systems (an inode number means nothing on another device) and, on almost every system, cannot point at directories (it would create cycles that .. could not resolve). Symbolic links have neither restriction because they are separate inodes whose data is a path string; that is why ls -l on a symlink shows a size equal to the target path’s length and why a symlink survives its target being deleted — as a dangling pointer.
The link count is also a correctness tool. Package managers and content-addressed stores (Nix, pnpm’s node_modules, Git’s object store on the same volume) deduplicate by hard-linking identical files; a build directory with 40,000 files can cost almost nothing in blocks. The trap is that editing "one" of them edits all of them — tools that care copy-on-write first (cp --reflink on btrfs/XFS/APFS does this at the block level).
Running out of inodes with space to spare
ext4 decides how many inodes exist when the file system is created: by default one inode per 16 KiB of space (-i in mkfs.ext4), stored in fixed inode tables inside each block group. A 100 GB volume gets about 6.5 million inodes. That is plenty for a workstation and hopeless for a mail spool, a PHP session directory, a CI cache, a Docker overlay directory or a node_modules tree with millions of 200-byte files: each consumes a whole inode and a whole 4 KiB block, and the inode table fills while most of the bitmap is still zero. touch then fails with ENOSPC — the same error as a full disk — and df -h shows space free.
df -i shows the inode count and usage; a IUse% of 100 with a Use% of 40 is the diagnosis. The cure is finding and deleting (or archiving into a single tarball) the directory with the most small files — find / -xdev -printf '%h\n' | sort | uniq -c | sort -rn | head — because you cannot add inodes to an existing ext4 volume without reformatting. XFS allocates inode chunks on demand and is limited only by a percentage of the volume (maxpct, default 25%); btrfs has no fixed inode limit at all. That is a real reason to choose XFS for a small-file-heavy volume.
The mirror-image failure is also real: a volume formatted with a large inode ratio for a media library cannot then host a source tree. Both are decided at mkfs time and both are invisible until they are not. tune2fs -l shows the numbers for ext4; check them before you commit a volume to a workload.
$ touch /var/cache/sessions/x touch: cannot touch '/var/cache/sessions/x': No space left on device $ df -h /var Filesystem Size Used Avail Use% Mounted on /dev/sdb1 200G 78G 122G 40% /var $ df -i /var Filesystem Inodes IUsed IFree IUse% Mounted on /dev/sdb1 13107200 13107200 0 100% /var
Key points
- The inode is the file: mode, owner, size, timestamps, link count, block map. Names live in directories as
(name, inode number)pairs. - Inode numbers are unique per file system, not per machine; identity is
(device, inode). - ext2/ext3 map blocks with direct, single-, double- and triple-indirect pointers; ext4, XFS, btrfs, APFS and NTFS use extents — one entry per contiguous run.
lnincrements the link count,rmdecrements it; the inode and its blocks are freed only at zero links and zero open descriptors.- ext4 fixes the inode count at
mkfs; millions of tiny files exhaust inodes whiledf -hshows free space.df -iis the diagnostic; XFS and btrfs do not have the fixed limit.
Why does this exist?
Mechanisms are answers to constraints. Open each question before reading the answer.
▸Why store the metadata in a separate record instead of in the directory entry?
So that one file can have several names and so that renaming does not copy metadata. FAT stores metadata in the directory entry, and as a result has no hard links and no way to keep a deleted-but-open file alive.
▸Why did file systems move from block pointers to extents?
Because storage and files both grew by a thousand-fold: a pointer per 4 KiB block made large files expensive to map, free and check, while extents describe a contiguous gigabyte in one entry and make sequential layout explicit.
▸Why does ext4 fix the inode count in advance?
Its inode tables sit at known offsets inside each block group so an inode number maps to a disk location with arithmetic — no index needed. The price of that simplicity is a limit chosen before the workload is known.
Inode explorer
inode #902 mode -rw-r--r-- uid 1000 gid 1000 size 16384 bytes blocks 4 links 1 atime 2026-08-25 09:12:03 mtime 2026-08-25 09:11:58 ctime 2026-08-25 09:11:58 direct [40 41 42 47 · · · · · · · ·] single indirect → ∅
How it fails
What the failure looks like from inside real software.
ENOSPCwith free space: inode table exhausted by millions of small files (sessions, cache, mail,node_modules).df -iconfirms; deleting large files does not help.- Backup or
ducounts a hard-linked tree many times over, or a restore silently turns links into independent copies that then drift apart. - Editing a file in a pnpm or Nix store changes every project that links it, because they share one inode.
- A large append-only log has 200,000 extents after a year; sequential reads have become random reads.
filefragshows it; preallocation would have prevented it. - A tool compares files by inode number across two mounts and declares unrelated files identical.
- Deleting a multi-terabyte file on an old pointer-based file system freezes the writer for many seconds while every indirect block is walked.