Postfix: a stuck mail queue and the inodes it holds in /var/spool/postfix

Linux medium postfixmailinodesdiskqueue
$ postqueue -p | tail -3
                                         nobody@localhost
-- 839 Kbytes in 3000 Requests.

What you see

Mail stops going out, and postqueue -p ends with a number that keeps climbing:

$ postqueue -p | tail -3
                                         nobody@localhost
-- 839 Kbytes in 3000 Requests.

df -h says there is plenty of room. Then one day something else fails with /var/spool/postfix/maildrop/2A4F1: No space left on device, and the two are the same incident: every message parked in the queue owns at least one inode, and the maildrop error is only where the shortage surfaced.

This article measures the second half of that sentence on a real machine, because the number is smaller than most people assume — and that changes what you should do about it.

Why a queue refuses to drain

Postfix decides between “try again later” and “give up” by the exit status of the delivery agent. local(8) runs mailbox_command and reads its exit code:

  • 75 (EX_TEMPFAIL) — temporary failure, the message moves to deferred and stays queued.
  • 0 — delivered.
  • any other non-zero — permanent failure, the message bounces immediately and never enters the queue.

That single line explains why half the recipes for filling a Postfix queue on the internet do nothing on a current system. The classic instruction is:

postconf -e 'mailbox_command = /bin/false'

/bin/false exits 1, which is a permanent failure. The queue stays empty and the sender gets a bounce. This is what that looks like, from the same lab:

$ postqueue -p | tail -1
Mail queue is empty

$ du -x --inodes -d1 /var/spool | sort -n | tail -2
4	/var/spool/anacron
44	/var/spool/postfix

Forty-four inodes: Postfix’s own directory skeleton and nothing else.

Reproduce it properly

Return 75 instead, so Postfix is forced to defer:

sudo -i
dnf install -y postfix
systemctl enable --now postfix

# accept mail for users that do not exist, so it is not rejected up front
postconf -e 'local_recipient_maps ='

# a delivery command that always fails temporarily
printf '#!/bin/sh\nexit 75\n' > /usr/local/bin/failtemp
chmod +x /usr/local/bin/failtemp
postconf -e 'mailbox_command = /usr/local/bin/failtemp'
systemctl reload postfix

for i in $(seq 1 3000); do
  printf 'Subject: test %s\n\nbody\n' "$i" | sendmail -f root@localhost nobody@localhost
done

Three thousand messages take one to three minutes. The result on Rocky 9 with Postfix 3.5.25:

$ postqueue -p | tail -3
                                         nobody@localhost
-- 839 Kbytes in 3000 Requests.

$ find /var/spool/postfix/deferred -type f | wc -l
3000

Note the size: 839 KB for 3,000 messages, about 280 bytes each. A queue file is an envelope plus a pointer to the body, so a stuck queue is almost never a block-space problem. It is an inode problem, and only on filesystems where inodes are scarce.

What 3,000 messages cost in inodes

du --inodes counts inodes instead of bytes, which is the only measurement that answers this:

$ du -x --inodes -d1 /var/spool | sort -n | tail -5
2	/var/spool/mail
3	/var/spool/at
4	/var/spool/anacron
6077	/var/spool/postfix
6092	/var/spool

The root filesystem went from 244,912 used inodes before the test to 250,780 after it — a rise of 5,868, of which /var/spool accounts for almost all.

Divide 6,077 by 3,000 and you get about two inodes per queued message, not one. The exact ratio depends on the Postfix version and on how many recipients a message has, so treat two as what this build did and measure your own rather than trusting the figure:

sudo sh -c 'for d in /var/spool/postfix/*/; do echo "$(find "$d" -type f | wc -l) $d"; done | sort -n | tail -5'

That prints a per-directory file count, which tells you whether your queue is concentrated in deferred or spread across maildrop, active and bounce — three different problems that all show up as “the queue is huge”.

Do the math for your filesystem

At two inodes per message, the number of messages needed to exhaust a filesystem is roughly half its inode count:

Filesystem inodesMessages to exhaustion
250,000 (small /var, mkfs -T small)~125,000
1,000,000 (default 8 GB ext4 volume)~500,000
23,635,968 (the Rocky 9 root in this lab)~11.8 million

That last row is the honest conclusion, and it is not the one the alarmist blog posts give you. On a modern root filesystem a stuck queue will not exhaust inodes. It would need eleven million messages, and maximal_queue_lifetime (5 days by default) throws messages away long before that unless mail arrives faster than it can be delivered.

Where the arithmetic does bite:

  • /var on its own small ext4 volume. 8 GB formatted with -T small gives you 250,000 inodes; a backscatter storm or a broken cron job sending one mail per event reaches that in days.
  • Containers and overlay filesystems, where /var/spool/postfix can live on a layer with a far smaller inode budget than the host.
  • Mail relays, where tens of thousands of messages per hour is normal traffic and a downstream outage parks all of it at once.
  • The same box also holding inode-hungry data — PHP sessions, ~/.npm/_cacache, package caches. The queue is then the last 2% that tips a filesystem already at 99%.

So the reason to monitor the queue is delivery, not disk. The disk case is real but specific, and knowing which one you are in tells you whether to purge aggressively or go find why nobody@localhost cannot be delivered.

Watch the depth, not just df

Queue depth is one command, and it works in cron:

postqueue -p | awk '/Requests/ {print $5; exit} !/Requests/ && /empty/ {print 0}'

Pair it with the inode check from the ext4 article, because they fail at different times:

df -i /var | awk 'NR>1 && $5+0 >= 80 {print $6, $5}'

Alert on the queue at a few hundred messages on a normal relay, or on any sustained growth on a single server. Growth is the signal; the absolute number depends entirely on what the box does.

Clear it without deleting the wrong mail

Deleting is the last step, not the first. Find out why delivery is failing:

postqueue -p | head -20                  # who is deferred, and to which recipient
postcat -q 2A4F1 | head -20              # full headers and the reason for the last attempt
grep -i 'deferred\|status=' /var/log/maillog | tail -20

Then pick a scope:

postqueue -f                    # retry everything now, no deletion
postsuper -h ALL                # hold, so nothing is retried and nothing is lost
postsuper -d ALL deferred       # delete only the deferred queue
postsuper -d ALL                # delete everything: incoming, active, deferred, hold

Two warnings worth repeating. postsuper -d ALL is not scoped to deferred and will delete messages that were about to be delivered; and deleting a bounce does not tell the sender anything, so a queue full of backscatter will simply refill until the source is blocked.

If one transport is the cause, take it out of service instead of deleting:

postconf -e 'defer_transports = smtp'
systemctl reload postfix

Verify

postqueue -p | tail -1

Mail queue is empty — or a count you can explain. Then confirm the filesystem actually recovered, and that new mail flows:

df -i /var
printf 'Subject: after\n\nbody\n' | sendmail -f root@localhost root@localhost
postqueue -p | tail -1