Bali, Indonesia
Insights

Check the Postfix Deferred Queue by Domain Every Week Before Tickets Pile Up

November 5, 2026
0:00
Most delivery tickets about Postfix start the same way: a customer says mail to one company has been slow for days, or a client notices replies never arrived. By then the deferred queue has usually been growing quietly for a while. Postfix keeps retrying, nothing crashes, and no alert fires because nothing has technically failed yet. Deferred mail is mail that hit a temporary failure. Postfix keeps it and retries on a backoff schedule until it is delivered or until it gets too old and bounces. That retry behavior is useful, but it also hides problems. A few minutes of looking at the queue once a week, grouped by domain, usually shows the pattern long before it turns into a stack of tickets.
There are two tools for this, and they answer different questions. postqueue -p (the same output as mailq) lists every queued message with its sender, recipients, and the last delay reason. That is good for detail and tedious for overview. qshape summarizes a queue as a grid: one row per domain, one column per age bucket. That is the view that shows whether one domain is soaking up most of the backlog. On some distros qshape comes with the main Postfix package. On others it lives in a separate package, often named something like postfix-perl-scripts. If the command is missing, check your package manager before assuming it is not available. These commands are read-only and safe to run on a live server. They usually need root.
BASH
# Total queue size (last line of the listing)
postqueue -p | tail -n 1

# Deferred queue grouped by recipient domain, busiest first
qshape deferred | head -n 15

# Same queue grouped by sender domain (useful for spotting a spam run)
qshape -s deferred | head -n 15

# Keep a dated copy so next week has something to compare against
qshape deferred > /root/qshape-deferred-$(date +%F).txt
If you move that last line into a cron job, put it in a small script and call the script from cron. Cron treats % as a special character, so a bare date +%F inside a crontab line will not behave the way you expect.
The first row is the total. Each row after that is a domain. T is the total count for that domain, and the numbered columns are age buckets in minutes. By default each bucket roughly doubles the previous one, but check man qshape on your version for the exact defaults.
TEXT
Illustrative output format (values are made up and kept round)

                  T   5  10  20  40  80 160 320 640 1280 1280+
         TOTAL  420   0   0   2   5  10  20  40  60   80   203
   example.net  300   0   0   0   0   5  10  30  50   60   145
   example.org   40   0   0   0   2   2   4   4   4   10    14
   example.com   20   0   0   2   3   3   6   6   6    4     0
The shape matters more than the totals. In this illustrative example, one recipient domain holds most of the deferred mail, and most of it sits in the oldest columns. That means mail to that domain has been failing for more than a day, not just during a brief hiccup. A domain with a few messages spread across the young columns is normal background noise. Remote servers go down, greylist, or throttle all the time, and Postfix handles that on its own. The sender view (qshape -s deferred) answers a different question. If one of your own local domains dominates the deferred queue and the recipients are scattered across hundreds of unrelated domains, that looks less like a remote problem and more like a compromised mailbox or a script sending spam that receivers are refusing. In that case the investigation moves to your side. Finding a Compromised Mailbox in the Postfix Queue After Moving From cPanel Exim covers that path. A useful rule of thumb for the weekly review is to ask three questions. Is any single domain clearly larger than the rest? Is its mail mostly in the old columns? Was it there last week too? A yes to all three is worth investigating today, even if no customer has complained yet.
The queue tells you where mail is stuck. The logs tell you why. Postfix writes the remote server's response, or its own error, in parentheses after status=deferred. Read that text instead of guessing. Log location depends on the system. Debian and Ubuntu with rsyslog typically use /var/log/mail.log, RHEL-family systems typically use /var/log/maillog, and systems that only run journald keep it in the journal. Adjust the path in the commands below to match your server.
BASH
# Which recipient domains are being deferred most often in the log
grep 'status=deferred' /var/log/mail.log | grep -o 'to=<[^>]*>' | cut -d@ -f2 | tr -d '>' | sort | uniq -c | sort -rn | head

# The actual deferral reasons for one domain (replace example.net)
grep 'status=deferred' /var/log/mail.log | grep 'example.net' | grep -o 'status=deferred (.*' | cut -c1-160 | sort | uniq -c | sort -rn | head

# On journald-only systems, read the smtp client messages from the last week
journalctl -t postfix/smtp --since '7 days ago' | grep 'status=deferred' | grep 'example.net' | tail -n 20

# Newer Postfix: JSON queue listing, grouped by the stored delay reason (needs jq)
postqueue -j | jq -r '.recipients[].delay_reason // empty' | cut -c1-160 | sort | uniq -c | sort -rn | head
The reasons often include an IP address or a remote queue ID, so lines that mean the same thing may not collapse perfectly in uniq -c. Read the top few lines and group them by eye. postqueue -j is only available in newer Postfix releases. If it errors out, check postconf mail_version and fall back to postqueue -p, which also prints the delay reason under each message. Avoid opening message bodies with postcat during a routine review unless you need the content for an abuse investigation. Most of the time, the headers and the log reason are enough.
The exact wording depends on the receiving server, so treat this as a starting map, not a lookup table.
Reason in the log (typical wording)What it usually points toWhere to look next
Connection timed out or Connection refused to the remote MXRemote server down, or outbound port 25 blocked somewhere on the pathTest a connection to the MX on port 25 from this server, then check firewall and provider egress rules
Host or domain name not foundDNS lookup failure for the recipient domain or its MXQuery the MX with dig from the server itself and check the resolver it uses
421 or 4.7.x mentioning rate, too many connections, or try again laterThe receiver is throttling youConcurrency and rate settings for that destination, and sending volume
450 or 451 mentioning greylistingExpected first-attempt rejection that clears on retryUsually nothing, unless the same message stays deferred for hours
4xx mentioning reputation, policy, or a blocklistThe receiver distrusts your IP or domainSender view in qshape, authentication records, and recent volume
The last row is where the sender view becomes useful again. A reputation deferral that appears in the same week that one local sender floods the queue is rarely a coincidence.
When something is fixed, such as a firewall rule, a DNS entry, a compromised password, or a throttle setting, the instinct is to run postqueue -f and watch the queue drain. That is sometimes fine. Against a receiver that is already rate-limiting you, though, retrying everything at once can make the deferrals worse. Should You Run postqueue -f or Let Postfix Retry Deferred Mail on Its Own? goes through that trade-off. A quieter way to confirm the fix is to compare before and after. Run qshape deferred again after the next few retry cycles and compare it with the dated snapshot you saved. If the fix worked, the stuck domain's count should shrink and stop gaining new messages in the young columns. If the young columns keep filling, new mail is still failing, and the fix did not address the real cause. Re-run the log grep for that domain and see whether the reason text has changed. A change in the reason is often the first sign of progress, for example when a timeout becomes a rate-limit response. Also keep queue lifetime in mind. Mail that stays deferred long enough eventually bounces back to the sender, and Postfix controls that window with maximal_queue_lifetime. Check your effective value with postconf maximal_queue_lifetime. A weekly review that only runs after the lifetime has already passed will miss messages that have already turned into bounces, so it is worth knowing how long that window is on your servers.
The review only helps if it happens on a dull week too. Keep it small: one qshape deferred, one qshape -s deferred, a dated copy, and a log grep for any domain that stands out. That fits in a few minutes. For a support team, it also changes the conversation. When a customer later asks why mail to a partner was slow, there is already a record of when the deferrals started and what the remote server said. As a practical takeaway, pick a fixed day and save the qshape output so each week can be compared with the last. When one domain grows in the old columns, read the deferral reason in the log before touching the queue. Most stuck-mail tickets can be answered from those two views, ideally before the customer has to ask.