In a relay or gateway Postfix setup, relay_recipient_maps validates recipients before mail enters the queue. When this map goes stale or responds slowly, you see one of two failure modes: valid recipients rejected with a 550 response (stale .db file, no queue entry to investigate), or smtpd processes hanging during RCPT TO because a network-backed lookup never returns (connection capacity drops, new connections queue or fail).

The same failure modes and fixes apply to local_recipient_maps.

What this means

relay_recipient_maps and local_recipient_maps are membership checks, not data lookups. When smtpd processes a RCPT TO, it queries the configured map. If the lookup returns a result, smtpd accepts. If it returns nothing, smtpd rejects with the code from unknown_relay_recipient_reject_code (default 550).

Critical subtlety: Postfix treats any returned value as “found,” including an empty string. To get “not found,” a SQL query must return zero rows. A stored function or query that always returns a row (even with a NULL or empty value) will cause Postfix to accept every recipient. This is the most common cause of relay_recipient_maps appearing to do nothing.

For hash:, btree:, and lmdb: maps, Postfix reads the compiled .db file on each lookup. Editing the source text file without running postmap leaves the old data in place. The .db file mtime is the definitive freshness indicator. postmap writes the .db atomically, so a successful rebuild takes effect on the next lookup without a Postfix reload.

For network-backed maps (mysql:, pgsql:, ldap:), each lookup is a live query. If the backend is slow or unresponsive, the smtpd process blocks until the backend responds or times out.

flowchart TD
    A["Valid recipient rejected or smtpd hung"] --> B{"Map type?"}
    B -->|hash/btree/lmdb| C["Check .db mtime vs source"]
    B -->|mysql/pgsql/ldap| D["Time a direct lookup"]
    C --> E{".db older than source?"}
    E -->|Yes| F["Rebuild with postmap"]
    E -->|No| G["Test postmap -q for address"]
    D --> H{"Lookup over 500ms?"}
    H -->|Yes| I["Check backend and timeouts"]
    H -->|No| J["Check for empty-result bug"]

Common causes

CauseWhat it looks likeFirst thing to check
Stale .db fileValid recipients rejected with 550 consistently; source file edited but postmap not runCompare .db mtime to source file mtime
Failed postmap rebuildSame as stale .db; postmap exited non-zero due to syntax error in sourceRun postmap manually and check exit code
SQL returning a row for every addressrelay_recipient_maps accepts all recipients; invalid addresses get throughRun the SQL query manually for a non-existent address
Network map with no timeoutsmtpd processes accumulate; sessions stall during RCPT TOTime a direct postmap -q against the network map
NFS stale file handleIntermittent lookup failures; “stale file handle” in kernel logsCheck dmesg for NFS errors
Database replication lagValid recipients rejected because replica has not caught upCheck replication lag on the database serving Postfix

Quick checks

# Check current relay_recipient_maps configuration
postconf -h relay_recipient_maps

# Compare .db file mtime to source file mtime
stat -c '%n: %y' /etc/postfix/relay_recipients /etc/postfix/relay_recipients.db 2>&1

# Test a lookup for a known-valid recipient (should print a value, exit 0)
postmap -q validuser@example.com hash:/etc/postfix/relay_recipients

# Test a lookup for a known-invalid recipient (should print nothing, exit 1)
postmap -q nonexistent@example.com hash:/etc/postfix/relay_recipients

# Time a network-backed map lookup
time postmap -q validuser@example.com mysql:/etc/postfix/mysql-relay.cf

# Check smtpd process count against maxproc
ps aux | grep smtpd | grep -v grep | wc -l

# Check for recent rejections
grep 'User unknown' /var/log/mail.log | tail -20

# Check reject code configuration
postconf -h unknown_relay_recipient_reject_code

How to diagnose it

  1. Identify the failure mode. Are valid recipients being rejected (550 responses), or are SMTP sessions hanging? Check the logs for both patterns:
# Recent rejections
grep 'NOQUEUE.*reject' /var/log/mail.log | tail -30

# On systemd-based hosts, use journalctl for time-filtered results:
# journalctl -t postfix/smtpd --since '10 minutes ago' --no-pager | grep 'NOQUEUE.*reject'

# smtpd timeouts or lost connections
grep -i 'timeout\|lost connection' /var/log/mail.log | tail -20
  1. Determine the map type. Check what relay_recipient_maps points to:
postconf -h relay_recipient_maps

If it references hash:, btree:, or lmdb: files, the problem is likely a stale .db. If it references mysql:, pgsql:, ldap:, or proxy: wrappers around those, the problem is likely a slow or misbehaving backend.

  1. For file-based maps, verify freshness. Compare the .db mtime to the source file:
stat -c '%n: %y' /etc/postfix/relay_recipients /etc/postfix/relay_recipients.db 2>&1

If the source file mtime is newer than the .db mtime, the map is stale.

  1. Verify that postmap can rebuild successfully. Run it manually and check for errors.

    WARNING: this modifies the .db file in production if it succeeds.

# Rebuild and check exit status (atomically replaces .db on success)
postmap /etc/postfix/relay_recipients && echo "OK" || echo "FAILED"

A syntax error in the source file causes postmap to exit non-zero without updating the .db. The stale .db remains silently.

  1. For network-backed maps, time the lookup. Test both a valid and invalid address:
time postmap -q validuser@example.com mysql:/etc/postfix/mysql-relay.cf
time postmap -q nonexistent@example.com mysql:/etc/postfix/mysql-relay.cf

Note: time postmap -q includes process startup overhead (~20-50ms). For hash/btree/lmdb maps, the actual lookup is sub-millisecond, so a high wall-clock time is mostly fork/exec cost. For network-backed maps, the query time dominates if the backend is slow. Anything over 500ms for a network map risks smtpd process exhaustion under load.

  1. For SQL-backed maps, verify query semantics. Run the raw query from the .cf file against the database for a non-existent address. The query must return zero rows for Postfix to treat the recipient as “not found.” A query or stored function that returns a row with an empty string, 0, or NULL will cause Postfix to accept the recipient.

  2. Check for proxy: map caching. If your map is wrapped in proxy: (common in chroot setups), the proxymap daemon may cache results. Changes to the underlying map data might not take effect until proxymap is restarted or Postfix is reloaded.

  1. Check NFS if maps are network-mounted. NFS stale file handles cause unpredictable lookup failures:
dmesg | grep -i 'nfs\|stale' | tail -20

Postfix has workarounds for some NFS-related false errors (rename, mkdir, link) but no workaround for stale file handles on map files.

Metrics and signals to monitor

SignalWhy it mattersWarning sign
.db mtime vs source mtimeDirectly indicates stalenessSource mtime newer than .db mtime
Map query latency (network)Slow lookups exhaust smtpd poolOver 500ms sustained
smtpd process count vs maxprocHung lookups tie up processessmtpd count near maxproc with low delivery rate
“User unknown” rejection rateSpike indicates false negatives from mapSudden increase from baseline
Acceptance of known-invalid recipientsIndicates SQL query returning rows for every addressInvalid recipients being accepted
Database/LDAP replication lagLagging replicas return stale dataLag exceeding expected threshold
NFS stale file handle countNetwork filesystem issues on map filesAny occurrence in kernel logs

Fixes

Stale .db file

Rebuild the map from source:

postmap /etc/postfix/relay_recipients

postmap writes the .db atomically. A successful rebuild takes effect on the next lookup without requiring a Postfix reload. If you are changing the relay_recipient_maps parameter value itself in main.cf (not just the map contents), run postfix reload or wait approximately one minute for the change to take effect.

For adding a single recipient without a full rebuild, use incremental mode:

echo "newuser@example.com OK" | postmap -i /etc/postfix/relay_recipients

Incremental mode reads entries from stdin and does not truncate the existing database.

Failed postmap rebuild

Run postmap manually and inspect the error output. Common causes include malformed entries (missing values, invalid characters, duplicate keys). Fix the source file, then rebuild and verify:

postmap /etc/postfix/relay_recipients && echo "rebuild OK" || echo "rebuild FAILED - check source file"

SQL query returning rows for every address

Modify the query or stored function so that non-existent recipients return zero rows. Postfix needs the database to return no matching rows for a “not found” result.

Check the query defined in the .cf file. A query like SELECT goto FROM alias WHERE address='%s' that matches no rows correctly returns “not found.” A stored function that always returns a value (even 0 or empty string) always returns “found.” Rewrite the query so it returns zero rows for non-existent recipients.

Slow or hanging network-backed map

Add or tune timeout configuration in the map .cf file. Check which timeout parameters your Postfix version supports for the map type (connect_timeout, etc.).

Additional options:

  • Use proxy: to share database connections across smtpd processes, reducing backend connection load.
  • Move hot recipient data to a local hash or lmdb map, rebuilt periodically from the authoritative source.

Verify the backend itself is healthy. A slow database affects every smtpd process that queries it, and the smtpd hang is a symptom of the backend problem, not a Postfix bug.

smtpd per-request deadline (Postfix 3.7+)

On Postfix 3.7 and newer, smtpd_per_request_deadline changes smtpd_timeout from a per-read/write timer to a combined per-request deadline. Enabling this can prevent individual smtpd sessions from hanging indefinitely on a slow map lookup.

NFS stale file handles

Postfix cannot work around stale file handles on NFS-mounted map files. Move the map to local storage, or implement a process that copies the source file locally and rebuilds the .db on the local filesystem.

Prevention

  • Monitor .db mtime against source mtime. Alert when the source file is newer than the compiled .db. This catches the most common failure: editing the source without rebuilding.
  • Run postmap in your deployment pipeline. Ensure postmap runs automatically after any source file change, and fails the deployment if postmap exits non-zero.
  • Test map lookups in CI. Verify that known-valid addresses return “found” (exit 0) and known-invalid addresses return “not found” (exit 1).
  • Set latency thresholds for network-backed maps. Alert when lookup time exceeds 500ms for SQL/LDAP maps.
  • Monitor smtpd process utilization. A rising smtpd count with flat delivery rate is an early sign of hung lookups.
  • Prefer local storage for map files. NFS adds stale-file-handle risk that Postfix cannot mitigate.
  • Verify SQL query semantics during development. Confirm that non-existent addresses produce zero-row results, not rows with empty values.

Monitoring with Netdata

When debugging recipient map problems, these Netdata signals are most useful:

  • smtpd process count: Track against maxproc in master.cf. A rising count with flat delivery rate signals hung lookups.
  • TCP latency to database/LDAP backends: Slow backend queries directly correlate with smtpd hangs during RCPT TO.
  • Disk I/O on map file storage: Relevant when hash/lmdb maps live on network-mounted storage.
  • Anomaly detection on Postfix rejection rates: Sudden spikes in “User unknown” responses indicate a stale or misconfigured map.

Correlating these signals across the same time window distinguishes Postfix configuration issues from backend problems.