Skip to content

How to find why an email bounced in Postfix

“So-and-so says they never got my email.” That is usually how this starts. The sender did not receive a bounce message (no non-delivery report), the recipient says it is not in their spam folder, and now you have to work out what actually happened.

The Postfix mail log is the source of truth. It records exactly what your server did with the message: handed it off to the recipient’s server, kept retrying it, or rejected it outright. This guide walks the diagnostic workflow: find the delivery attempt, read what happened, and when it failed, decode the reason.

Search the mail log for the recipient address or the Postfix queue ID. Queue IDs are typically 10-16 alphanumeric characters (example: E1234567890ABC).

To find by recipient:

Terminal window
grep "to=<user@example.com>" /var/log/mail.log | tail -20

To find by queue ID:

Terminal window
grep "E1234567890ABC" /var/log/mail.log

Look for a line with the smtp or lmtp service. You will see something like this:

Jun 17 10:22:33 mail postfix/smtp[12345]: E1234567890ABC: to=<user@acme.io>, relay=mail.acme.io[192.0.2.1]:25, delay=0.45, delays=0.01/0.02/0.15/0.27, dsn=5.1.1, status=bounced (550 5.1.1 <user@acme.io> user unknown)

This single line contains everything you need.

a delivery log line
postfix/smtp: 9F2A1C0A2B: to=<a@acme.io>, relay=mx.acme.io[198.51.100.7]:25, delay=2.1, dsn=5.1.1, status=bounced (550 5.1.1 user unknown)
queue id ties every line for this message together
to= the recipient (every recipient is logged this way)
delay= seconds from arrival to this delivery attempt
dsn= RFC 3463 enhanced status code: the structured reason
status= sent, deferred, or bounced
( ... ) the remote server's raw SMTP reply

Before decoding anything, read the status= field. It puts the message in one of four situations, and it decides whose problem this is:

  • status=sent: your server handed the message off and the recipient’s server accepted it (a 250 reply). It left your infrastructure successfully. If the recipient still does not see it, the problem is downstream: their spam filtering, a mailbox rule, or a silent discard on their side. The log line is your proof of handoff (relay host, IP, timestamp, and the accepting 250 response).
  • status=deferred: a transient failure. The message is still queued and Postfix is retrying. It has not arrived yet, but it has not failed permanently either.
  • status=bounced: a permanent failure. Postfix gave up and generated a non-delivery report to the sender. If the sender never saw that report, it was filtered or routed elsewhere, but the log still has the reason.
  • No matching line at all: the message never reached this server. The problem is upstream, at submission or on whichever host the client actually sent through. Investigate that host instead.

The most common surprise is status=sent for a message the recipient swears never arrived. That is not a Postfix delivery failure: your server did its job and has the receipt to prove it, so the next step is the recipient’s side, not yours. The rest of this guide covers the case where the log shows a failure and you need the reason.

The status= field tells you whether Postfix gave up permanently or will retry:

  • status=bounced: a PERMANENT failure. Postfix will not retry. A delivery status notification (DSN) is sent back to the original sender.
  • status=deferred: a TRANSIENT (temporary) failure. Postfix will retry later.

The dsn= field is the Enhanced Status Code (RFC 3463). This is a structured 3-part code that classifies the failure type. The first digit tells you the category:

  • 2.x.x: Success. (You will not see this in a bounce.)
  • 4.x.x: Persistent transient failure. Postfix will retry.
  • 5.x.x: Permanent failure. This is a bounce. Postfix gives up.

In our example, dsn=5.1.1 starts with 5, so the status is permanent. Postfix will not retry and will generate a bounce message to the sender.

Everything in parentheses at the end is the remote mail server’s SMTP response. This is the most human-readable part:

(550 5.1.1 <user@acme.io> user unknown)

The first number (550) is the 3-digit SMTP status code (RFC 5321). The second number (5.1.1) echoes the Enhanced Status Code. Everything after that is the server’s text explanation.

Here are the most common permanent failure codes (all start with 5):

  • 5.1.1: Bad destination mailbox (user unknown, no such user). The email address does not exist on the receiving server.
  • 5.2.2: Mailbox full, permanent. The mailbox quota is exceeded and cannot accept mail. Some servers treat this as transient (4.2.2) and will retry; others make it permanent.
  • 5.7.1: Delivery not authorized (blocked by policy). The receiving server rejected the message due to a policy rule, such as SPF/DKIM/DMARC failure, rate limiting, or explicit blocklist. When a 5.7.1 points at authentication, paste the bounced message’s headers into the Email Header Analyzer to see which of SPF, DKIM, or DMARC failed.
  • 5.4.4: Unable to route. The receiving domain has no reachable mail server (a DNS or MX lookup failed, or there is no route to the host). Often means the domain is misspelled or no longer accepts mail.

The remote server’s text varies by implementation. A Postfix server might say user unknown; another system might say no such user or invalid recipient. Both indicate 5.1.1.

Note: The codes and text come from the remote server, not from your Postfix instance. You cannot change them; they reflect the receiving server’s decision.

If you see status=deferred with a dsn=4.x.x code, Postfix is retrying. Examples:

  • 4.4.2 (connection dropped): The server disconnected mid-transaction. Postfix will retry.
  • 4.7.1 (temporary policy restriction): The server temporarily rejected the mail. Postfix will try again.

Postfix retries deferred mail according to its queue configuration (default: several attempts over 5 days). You can force a retry with postqueue -i or allow it to timeout.

Only status=bounced with a 5.x.x code generates a DSN back to the sender. The DSN itself is an email message (RFC 3464) that includes the recipient address, the DSN codes, the remote server’s text, and a copy of the original message headers. Your Postfix bounce daemon generates and sends this DSN.

You receive a complaint: “My email to compliance@nope.org did not arrive.”

  1. Search the log:

    Terminal window
    grep "to=<compliance@nope.org>" /var/log/mail.log | grep bounced
  2. You see:

    Jun 17 11:15:22 mail postfix/smtp[23456]: F9876543210XYZ: to=<compliance@nope.org>, relay=mx.nope.org[203.0.113.42]:25, delay=1.23, delays=0.05/0.10/0.85/0.23, dsn=5.1.1, status=bounced (550 5.1.1 user unknown)
  3. You can now tell the sender: “Our system tried to deliver to compliance@nope.org at nope.org’s mail server. The server rejected it with a ‘550 5.1.1 user unknown’ error. That address does not exist on their server. Please verify the address and resend.”

If you see dsn=4.4.2 with status=deferred instead, you would say: “The delivery is queued and being retried. The connection was temporarily dropped. Check back later or contact nope.org if the issue persists.”

Using Postfix Insights to speed up diagnostics

Section titled “Using Postfix Insights to speed up diagnostics”

Reading the log by hand is straightforward, but Postfix Insights automates the correlation. The tool:

  • Extracts the queue ID from any message you search for.
  • Groups all lines for that message (from pickup, cleanup, qmgr, smtp, etc.) in one view.
  • Highlights the recipient status and DSN code for quick scanning.
  • Shows the raw log line so you can read the remote server’s reply without grepping.
  • Lets you search by recipient, sender, queue ID, or domain.

This means no more manual grepping or context-switching between terminals. For a sysadmin managing thousands of messages a day, that time adds up.

Get started with Postfix Insights. The source code and issue tracker are at https://github.com/Xodus-CO/postfix-insights.