df Command in Linux: Complete Guide to Disk Space Reporting and Parameters

df command in Linux and it perimeters

df is usually the very first command I run when a server alert fires for low disk space, and it’s also usually the first command I run on any new system just to get oriented — what’s mounted, how big, how full. It looks trivially simple, but there are a handful of details (inode exhaustion, the difference between df and du, tmpfs accounting) that catch people out regularly. Here’s the complete picture.

What df Does

df (disk free) reports filesystem-level space usage: total size, used space, available space, and use percentage, for every mounted filesystem, or for a specific path you point it at. Unlike du, which walks a directory tree file by file, df reads accounting data the kernel already maintains for each mounted filesystem — which is why it returns instantly even on enormous filesystems.

Syntax

df [OPTION]... [FILE]...

Core Options

OptionLong formDescription
-h--human-readableSizes in K/M/G/T (powers of 1024)
-H--siSizes in powers of 1000 instead of 1024
-T--print-typeShow the filesystem type column
-i--inodesReport inode usage instead of block usage
-a--allInclude pseudo/dummy filesystems normally hidden (like proc)
-x TYPE--exclude-type=TYPESkip filesystems of the given type
-t TYPE--type=TYPEOnly show filesystems of the given type
--totalPrint a grand total row across all listed filesystems
--output=FIELDSChoose exactly which columns to display
-l--localOnly show locally-mounted filesystems, skipping network mounts

Tested Examples

I ran these directly on a live system to capture real output:

Default output

$ df -h
Filesystem      Size  Used Avail Use% Mounted on
tmpfs           2.0G     0  2.0G   0% /dev/shm
tmpfs           2.0G     0  2.0G   0% /sys/fs/cgroup
/dev/vda        252G  8.6G   10G  47% /

With filesystem type shown

$ df -T /home
Filesystem     Type 1K-blocks    Used Available Use% Mounted on
/dev/vda       ext4 264212084 8971640  10460896  47% /

Inode usage

$ df -i /home
Filesystem       Inodes  IUsed    IFree IUse% Mounted on
/dev/vda       16777216 193982 16583234    2% /

Custom column selection

$ df --output=source,fstype,size,used,avail,pcent,target /home
Filesystem     Type 1K-blocks    Used    Avail Use% Mounted on
/dev/vda       ext4 264212084 8971640 10460896  47% /

--output is genuinely underused — it’s the cleanest way to script against df output without fighting awk column-position parsing that breaks the moment filesystem names get long.

Reading the Percentage Correctly

Notice in the example above: the filesystem is 252G total, 8.6G used, but only 10G available — not 243G. That’s not a bug. ext4 (like most Linux filesystems) reserves a percentage of total space (5% by default) exclusively for root, so unprivileged processes see less “available” space than the raw arithmetic would suggest, even though df‘s “Use%” column is calculated against the space non-root users can actually use, not the raw total. This reservation exists specifically so that a runaway process filling the disk as a normal user doesn’t leave root without enough headroom to log in and fix things.

You can inspect and adjust that reserved percentage with tune2fs:

tune2fs -l /dev/vda | grep -i reserved
tune2fs -m 1 /dev/vda   # reduce reserved space to 1%

The Inode Trap

This is the single most common “df says there’s space but I can’t create files” scenario I’ve debugged. Every filesystem has a finite number of inodes — metadata structures, one required per file or directory regardless of that file’s size. If you create millions of tiny files (a classic symptom: a misbehaving cache directory, a mail spool with thousands of tiny message files, or a logging system writing one file per event), you can exhaust inodes while block space usage still looks fine.

$ df -h /var
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda        50G   12G   36G  25%  /var

$ df -i /var
Filesystem      Inodes  IUsed   IFree IUse% Mounted on
/dev/vda       3276800 3276800      0  100% /var

Same filesystem, two completely different pictures. Whenever “no space left on device” shows up despite df -h looking fine, df -i is the very next command I run.

How df Works Internally

df calls statfs()/statvfs() on each mounted filesystem, a single system call that returns a struct statvfs populated by the specific filesystem driver — total blocks, free blocks, available blocks (free minus reserved), total inodes, free inodes, and block size. Because this is one syscall returning pre-maintained kernel accounting rather than a directory traversal, df is essentially instantaneous even on filesystems holding petabytes of data — a sharp contrast with du, which must walk every file.

df reads the list of currently mounted filesystems from /proc/mounts (or /etc/mtab historically) to know what to query, which is also why network filesystems that have gone stale can cause df to hang: the statvfs() call itself blocks waiting on a network round trip that will never complete, and no amount of local kernel accounting can route around a genuinely unresponsive NFS server.

Real-World Sysadmin Workflow

Monitoring script for alerting

#!/bin/bash
set -euo pipefail
THRESHOLD=85

df -hP --exclude-type=tmpfs --exclude-type=devtmpfs | tail -n +2 | while read -r line; do
  usep=$(echo "$line" | awk '{print $5}' | tr -d '%')
  target=$(echo "$line" | awk '{print $6}')
  if [ "$usep" -ge "$THRESHOLD" ]; then
    echo "WARNING: $target is at ${usep}% capacity"
  fi
done

-P (POSIX format) is worth knowing here specifically for scripting: it guarantees each filesystem’s info stays on a single line, avoiding a formatting quirk where df wraps very long device names (common with LVM or device-mapper paths) onto a second line, which silently breaks naive awk-based parsers.

Checking available space before a deployment

AVAIL_KB=$(df --output=avail /var/lib/docker | tail -1 | tr -d ' ')
REQUIRED_KB=$((5 * 1024 * 1024))  # 5 GB
if [ "$AVAIL_KB" -lt "$REQUIRED_KB" ]; then
  echo "Not enough space for deployment, aborting."
  exit 1
fi

I add checks like this to CI/CD deploy scripts on self-hosted runners regularly — cheap insurance against a deploy failing halfway through because a Docker image pull filled the disk.

Troubleshooting

df hangs indefinitely — almost always a stale/unreachable network mount (NFS, CIFS). Identify with df 2>&1 & and ps to see which mount it’s stuck on, or check /proc/mounts against known network shares; forcibly unmount with umount -f or umount -l (lazy unmount) once you’ve confirmed which one is unresponsive.

“No space left on device” but df shows free space — check inodes with df -i as covered above; also check for a deleted-but-open file holding space (lsof | grep deleted), which df counts as used but no path can be found for it.

df and du disagree significantly — expected in several legitimate cases: reserved root blocks, deleted-but-open files, sparse files, or a bind mount/overlay filesystem where du walking one path double counts or undercounts relative to the actual backing filesystem’s df accounting.

Filesystem shows 100% but was recently deleted from — some filesystems (notably ext4 with a very full journal, or copy-on-write filesystems like Btrfs/ZFS with snapshots) don’t immediately reflect freed space; Btrfs in particular can report confusing numbers because of how it allocates chunks ahead of actual usage — btrfs filesystem usage <mountpoint> gives a far more accurate picture than plain df on Btrfs.

Performance Optimization

  • Avoid df in tight monitoring loops against network filesystems — cache results and re-check on a reasonable interval (30–60s) rather than calling df on every request in a hot path, since a slow or degraded NFS server can turn a cheap syscall into a multi-second stall.
  • Use --local in monitoring scripts running across mixed environments to skip network mounts entirely when you only care about local disk health, avoiding hangs altogether.
  • Prefer --output over parsing default columnar output — it’s faster to write correct scripts against and avoids fragile column-position assumptions.

df vs Related Commands

CommandPurpose
dfFilesystem-level free/used space, from kernel accounting, instant
duDirectory-tree-level actual disk usage, computed by walking files
lsblkBlock device and partition layout, doesn’t show usage percentages
mountShows currently mounted filesystems and their options, not usage
findmntModern, more flexible mount-table inspection tool, can filter and format similarly to df --output
btrfs filesystem usage / zfs listFilesystem-native usage reporting for copy-on-write filesystems where plain df numbers can be misleading

Security Implications

df itself is a read-only, low-risk command — any user can run it, and it doesn’t expose sensitive file contents, only aggregate space figures. The main operational risk is indirect: automated monitoring that shells out to df against user-influenced or network-mounted paths can be a denial-of-service vector if an attacker can force it to query an unresponsive network filesystem repeatedly, hanging monitoring workers. Timeouts around any df call against non-local mounts are worth building into automation from the start rather than adding after the first incident.

Distribution Compatibility

df is part of GNU coreutils and behaves identically across essentially all mainstream distributions — Debian, Ubuntu, Fedora, RHEL/CentOS, Arch, openSUSE. The main differences you’ll encounter are default output formatting from locale settings, and whether pseudo-filesystems like tmpfs, overlay (common inside containers), or squashfs (common on live/rescue media) are shown by default — this is controlled purely by whether they’re mounted, not by distribution-specific df behavior. BusyBox’s df (Alpine, embedded systems) supports the core flags (-h, -T, -i) but has a smaller option set than GNU’s, so heavily flag-dependent scripts should be checked against BusyBox if portability to Alpine containers matters.

df Inside Containers: A Common Point of Confusion

Running df inside a Docker or Podman container can produce genuinely misleading results if you’re not aware of how container storage drivers work. df -h inside a container typically reports the size of the host’s underlying filesystem backing the container’s writable layer (overlay2, on most modern setups), not a size limit specific to that container — a container isn’t normally given its own dedicated, independently-sized filesystem unless you’ve explicitly configured storage quotas or are using a driver that supports per-container size limits (like devicemapper with dm.basesize, now largely deprecated in favor of overlay2’s own storage-opt size limiting).

docker run --rm alpine df -h

This will typically show the host’s root filesystem size, which can make a container appear to have far more space “available” than any quota you might have intended to enforce. If you need actual enforced per-container space limits, that’s a job for --storage-opt size=10G (where supported by your storage driver) or Kubernetes resource/ephemeral-storage limits — df alone inside the container won’t reflect an enforced ceiling unless one of these mechanisms is actually configured.

Understanding Block Size Reporting Nuances

By default, df‘s non-human-readable output reports in 1024-byte (1K) blocks, a detail that occasionally trips up scripts written against raw numeric output expecting bytes:

$ df /home
Filesystem     1K-blocks    Used Available Use% Mounted on
/dev/vda        264212084 8971640  10460896  47% /

That 264212084 is in units of 1024 bytes, not raw bytes — multiply by 1024 to get an actual byte count, or better, just use --block-size=1 or -B1 if you need raw bytes directly rather than doing the multiplication yourself:

df -B1 /home

I’d always recommend --output combined with an explicit --block-size in any script parsing df output programmatically, rather than relying on the default column formatting, which can vary subtly by locale and coreutils version.

Summary

df answers the question “how full is this filesystem” instantly because it reads kernel-maintained accounting rather than walking files, which is exactly what makes it the right first command when disk space alerts fire, and exactly why it can diverge from what du reports on the same filesystem. Between the reserved-block behavior on ext4 and the inode-exhaustion trap, most of the confusion people run into with df comes down to knowing these two things exist — once you do, its output stops being mysterious.

References

  • GNU Coreutils Manual — df invocation: https://www.gnu.org/software/coreutils/manual/html_node/df-invocation.html
  • Linux man-pages — df(1): https://man7.org/linux/man-pages/man1/df.1.html
  • Linux man-pages — statvfs(2): https://man7.org/linux/man-pages/man2/statvfs.2.html
  • Red Hat Documentation — Managing Storage: https://access.redhat.com/documentation/
Total
0
Shares

Leave a Reply

Previous Post
su command in Linux and it perimeters

su Command in Linux: Complete Guide to Switching Users, Superuser Access, and Parameters

Next Post
du command in Linux and it perimeters

du Command in Linux: Complete Guide to Disk Usage Estimation and Parameters

Related Posts