Take a weekly snapshot with postqueue and qshape
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% as a special character, so a bare date +%F inside a crontab line will not behave the way you expect.
Reading the qshape grid
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 0qshape -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.
Find out why that domain is deferred
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 | headuniq -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.
What the common deferral reasons usually point to
| Reason in the log (typical wording) | What it usually points to | Where to look next |
|---|---|---|
Connection timed out or Connection refused to the remote MX | Remote server down, or outbound port 25 blocked somewhere on the path | Test a connection to the MX on port 25 from this server, then check firewall and provider egress rules |
Host or domain name not found | DNS lookup failure for the recipient domain or its MX | Query 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 later | The receiver is throttling you | Concurrency and rate settings for that destination, and sending volume |
| 450 or 451 mentioning greylisting | Expected first-attempt rejection that clears on retry | Usually nothing, unless the same message stays deferred for hours |
| 4xx mentioning reputation, policy, or a blocklist | The receiver distrusts your IP or domain | Sender view in qshape, authentication records, and recent volume |
Verify the fix by comparing snapshots, not by flushing
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.
Making it a habit that actually sticks
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.