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
| Cause | What it looks like | First thing to check |
|---|---|---|
| Stale .db file | Valid recipients rejected with 550 consistently; source file edited but postmap not run | Compare .db mtime to source file mtime |
| Failed postmap rebuild | Same as stale .db; postmap exited non-zero due to syntax error in source | Run postmap manually and check exit code |
| SQL returning a row for every address | relay_recipient_maps accepts all recipients; invalid addresses get through | Run the SQL query manually for a non-existent address |
| Network map with no timeout | smtpd processes accumulate; sessions stall during RCPT TO | Time a direct postmap -q against the network map |
| NFS stale file handle | Intermittent lookup failures; “stale file handle” in kernel logs | Check dmesg for NFS errors |
| Database replication lag | Valid recipients rejected because replica has not caught up | Check 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
- 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
- Determine the map type. Check what
relay_recipient_mapspoints 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.
- For file-based maps, verify freshness. Compare the
.dbmtime 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.
Verify that postmap can rebuild successfully. Run it manually and check for errors.
WARNING: this modifies the
.dbfile 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.
- 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.
For SQL-backed maps, verify query semantics. Run the raw query from the
.cffile 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.Check for proxy: map caching. If your map is wrapped in
proxy:(common in chroot setups), theproxymapdaemon may cache results. Changes to the underlying map data might not take effect until proxymap is restarted or Postfix is reloaded.
- 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
| Signal | Why it matters | Warning sign |
|---|---|---|
| .db mtime vs source mtime | Directly indicates staleness | Source mtime newer than .db mtime |
| Map query latency (network) | Slow lookups exhaust smtpd pool | Over 500ms sustained |
| smtpd process count vs maxproc | Hung lookups tie up processes | smtpd count near maxproc with low delivery rate |
| “User unknown” rejection rate | Spike indicates false negatives from map | Sudden increase from baseline |
| Acceptance of known-invalid recipients | Indicates SQL query returning rows for every address | Invalid recipients being accepted |
| Database/LDAP replication lag | Lagging replicas return stale data | Lag exceeding expected threshold |
| NFS stale file handle count | Network filesystem issues on map files | Any 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
maxprocinmaster.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.
Related guides
- Postfix check warnings: configuration drift and permission problems
- How Postfix actually works in production: a mental model for operators
- Postfix mail flow: injection rate outpacing delivery rate
- Postfix active queue saturation: hitting qmgr_message_active_limit
- Postfix deferred queue growing: why mail piles up and how to drain it






