Guide

How to Get an Alert When a Disk Is Full on Linux (Before It Takes the Server Down)

A copy-paste df + cron alert script, plus the failure mode most guides skip: a failing box can't send its own alert.

PulseNode · updated July 2026

You want to get an alert when a disk is full on Linux before anything breaks, because a full disk breaks a lot at once. Postgres stops accepting writes, application logging dies, apt and Docker pulls fail, half-written files corrupt. And the obvious fix has a catch: the box that is supposed to email you is the box that is failing, and a full disk has no room left for its own mail spool.

The quick answer: a df + cron disk alert script

Here's a self-contained disk full alert email script. It checks every mounted filesystem and emails you when any one crosses a threshold.

The script

#!/usr/bin/env bash
# disk-alert.sh: email an alert when any filesystem crosses THRESHOLD% used.
# Save to /usr/local/bin/disk-alert.sh

THRESHOLD=90                 # alert at or above this percent
ALERT_TO="you@example.com"   # where the alert goes
HOST=$(hostname)

# df -P  → POSIX output: ONE line per filesystem, space-separated.
#          (Without -P, long device names wrap onto two lines and break awk.)
# awk    → skip the header (NR>1), strip the "%" off the Use% column ($5),
#          and print "<use> <mount>" only for filesystems at/over THRESHOLD.
#          $5 is Use%, $6 is the mount point in df -P output.
df -P | awk -v t="$THRESHOLD" 'NR>1 { gsub(/%/,"",$5); if ($5+0 >= t) print $5, $6 }' \
| while read -r use mount; do
    echo "${HOST}: ${mount} is ${use}% full (threshold ${THRESHOLD}%)." \
      | mail -s "DISK ALERT: ${mount} at ${use}% on ${HOST}" "$ALERT_TO"
done

That's the whole thing. Two details keep it from breaking the way the classics do. df -P (POSIX mode) forces one filesystem per line; the old df | awk '{print $5}' scripts floating around since 2010 read the percentage off the wrong column the moment a long device name wraps onto a second line. And $5+0 coerces the stripped value to a number, so the comparison is numeric rather than string.

To skip pseudo-filesystems like tmpfs, add excludes: df -P -x tmpfs -x devtmpfs -x squashfs.

Schedule it with cron

Make it executable and add a cron entry:

# Make the script runnable
chmod +x /usr/local/bin/disk-alert.sh

# Open your crontab
crontab -e

# Add this line to run the check at the top of every hour:
@hourly /usr/local/bin/disk-alert.sh

One prerequisite people miss: the mail command often isn't installed. On Debian/Ubuntu it comes from sudo apt install mailutils; on RHEL/Alma/Fedora it's sudo dnf install s-nail (the old mailx package only exists up to RHEL 8). And mail still needs a working mail transfer agent behind it, such as Postfix, msmtp, or an SMTP relay, or the message goes nowhere. A fresh server usually has none of those.

Why your disk-full alert didn't fire

The script above is correct. It will still let you down in production, usually for one of these reasons. The old df | awk | mail tutorials stop here.

mail isn't installed, or the relay drops the message

Run from cron, mail -s ... either fails with "command not found", which you never see, or hands the message to a local MTA with no relay configured, where it queues forever or gets dropped. Either way the script exits cleanly and no email arrives.

Cron has nowhere to report the failure

When a cron job writes to stdout or stderr, cron tries to mail that output to the local user through the system MTA. With no MTA installed, cron discards the output and logs "No MTA installed, discarding output" to syslog, which nobody watches either. With a local-delivery-only MTA, the output piles up in /var/mail/<user>, a mailbox no human opens. (mail itself dumps failed sends to a dead.letter file.) The one channel that could tell you your alert script is broken is a dead drop.

A full disk can't send its own alert

This is the trap the whole approach sits on: the alert is generated by the machine that is failing. When the disk holding /var/spool fills up, the local MTA has no room to enqueue the outgoing message, and logging is failing for the same reason. A failing host is a bad place to run its own failure alarm.

It doesn't scale past a couple of servers

Copy disk-alert.sh to twenty boxes and you own twenty crontabs and twenty MTA configs, each free to rot on its own schedule. Changing the threshold means editing twenty files, and nothing tells you whether the script is still running on all of them.

No history, no trend

A threshold alert says the disk is at 90% now. It can't say whether usage crept up over three weeks or a runaway log file added 40% in an hour, and those are different emergencies: rotate a log this week versus page someone now.

A sturdier DIY: push the check off the box

The fix costs nothing: it's the heartbeat pattern, also called a dead man's switch.

Flip the direction: the host runs the same df check and pushes the result outward, a small curl to an external endpoint every few minutes carrying "disk on / = 87%". Something off the box (your own watcher on another machine, or a service like Healthchecks.io) then alerts you in two cases: a bad value arrived, or nothing arrived because the host went silent.

That second case is the one a local cron script structurally can't cover. A dead box sends nothing, and in a heartbeat setup, silence is what trips the alarm.

You can wire this together with curl, a cron entry, and an external endpoint. The cost: the watcher, the alert routing, and any history are now yours to run, a second small system to babysit.

Or skip the plumbing: hosted threshold alerts

Here's the pitch, so weigh it accordingly. PulseNode is the same off-box pattern, hosted. You install a single outbound-only agent with one command; it samples disk usage per mount, about every 30 seconds, and pushes the numbers to the dashboard. The rule you write is plain arithmetic:

disk > 90%      cooldown 60 min

The disk metric tracks your fullest mount, so a /var that quietly climbs to 91% trips the rule even while / sits at 40%. When the line is crossed on the next push, PulseNode sends the alert from its own servers, to Telegram and email, so delivery never depends on the failing box. If the agent goes silent instead, because the disk wedged the whole machine, you get an agent-offline alert. Cooldowns are per rule (1 to 1440 minutes, default 15) and a recovery notice goes out when the value drops back under the line, so one bad night is one ping, not forty. History is included: when an alert fires, you can pull up the chart and see how fast the disk was filling.

One caveat: the agent pushes about every 30 seconds, so an alert fires on the first push after the threshold is crossed. That's a trend detector, not a sub-second tripwire, and the right granularity for a disk filling over hours or days.

PulseNode is paid: Hobby is €5/mo for up to 5 servers, Pro €15/mo for 20. If you run one box and the bash script above covers you, keep it; it works and costs nothing. PulseNode starts to make sense at several servers, or when the alert has to survive the exact failure it's warning about.

Start monitoring disk space for €5/mo →

Which disks (and which kind of "full") to watch

"The disk is full" is vaguer than it sounds. Watch these:

Then there's the kind of "full" that df -h will never show you: inode exhaustion.

Every file and directory consumes one inode, a small metadata record, regardless of size. Many filesystems, ext4 included, fix the inode count at creation (XFS and Btrfs allocate inodes dynamically and mostly dodge this). Pile up millions of tiny files (PHP sessions, mail-queue fragments, cache shards) and you can run out of inodes while the disk is only 20% full by space. Writes then fail with the same No space left on device error while df -h insists there's plenty of room. Check it with:

df -i        # shows IUse%, inode usage per filesystem

If IUse% is near 100%, that's your problem, and a block-percentage alert sailed right past it. Watch both numbers: blocks with df -h, inodes with df -i.

Good thresholds without alert fatigue

The fastest way to teach yourself to ignore disk alerts is to set them badly. Warn at 80%: enough runway to rotate logs, prune images, or grow the volume calmly. Go critical at 90%, because on a busy filesystem the last 10% disappears fast. Apply the same 80/90 lines to inode usage (IUse%).

Two settings keep the noise survivable. Cooldowns, because a filesystem hovering at 91% will otherwise re-trigger on every run, and one alert per threshold per hour is plenty. And recovery notices: when usage drops back under the line, a short "recovered" note closes the loop and doubles as proof that your alerting still works.

FAQ

How do I check disk usage from the command line?

Use df -h for human-readable block usage per filesystem, df -i for inode usage (the kind that can fail even when space looks free), and du -sh /path to find what is eating a particular directory. du -sh --inodes /path/* breaks down inode usage by subdirectory.

What disk percentage should trigger an alert?

80% for a warning and 90% for critical is a sane default: enough headroom to act before writes start failing. On fast-filling filesystems you may want to warn earlier. Also alert on inode usage (df -i) at the same thresholds, because a disk can hit 100% inodes while only 20% full by space.

Why didn't I get my disk-full alert?

Three usual causes: the mail command was never installed, so nothing was ever sent; the local mail transfer agent has no working relay, so the message sits in a queue or a local mailbox nobody reads; or the disk was too wedged to send anything at all, because a failing box cannot write its own mail spool. The fix is to generate the alert off the affected machine.

How do I monitor disk space across multiple servers?

A per-box cron script does not scale: every server is its own crontab and its own silent failure. The stronger pattern is to push each host's disk usage to one central place that alerts on a bad value or on silence. You can build that with curl plus an external watcher, or use a hosted dashboard that does it for you.

See also