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: A SQL lookup counts as “found” only when the query returns a row with a non-empty value. To get “not found,” the query must return zero rows. A stored function or query that always returns a row with a non-empty value (even 0) will cause Postfix to accept every recipient; an empty or NULL result logs lookup ... returns an empty string result and is treated as not found. 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. Postfix 3.9 made the mysql:/pgsql: client’s hard-coded idle and retry timers configurable as idle_interval and retry_interval (both default 60s) in the map .cf file.
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 always returns a row with a non-empty value (even0) will cause Postfix to accept the recipient; an empty or NULL result logslookup ... returns an empty string resultand is treated as not found.Check how
proxy:maps behave. If your map is wrapped inproxy:(common in chroot setups), theproxymapdaemon shares one open lookup table among Postfix processes; it does not cache lookup results. A file-based table behindproxy:(for exampleproxy:hash:) stays open in proxymap, so a rebuilt.dbfile may not take effect until proxymap is restarted.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 (present since the late 1990s, no meaningful minimum version):
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 non-empty value (even 0) always returns “found”; an empty or NULL value is logged as lookup ... returns an empty string result and treated as not 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. For mysql:/pgsql: maps on Postfix 3.9+, the available settings are idle_interval and retry_interval; LDAP maps have their own timeout setting.
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 (default normal: no, overload: yes). Enabling it (smtpd_per_request_deadline = yes) can prevent individual smtpd sessions from hanging indefinitely on a slow map lookup; it is already active under overload by default.
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





