Your newsletter to 40,000 Gmail recipients should be done in ninety seconds. It isn't, because 300 messages to one corporate Exchange that greylists everything are sitting at the front of the same active queue, grabbing delivery slots on every retry. That is head-of-line blocking, and no amount of default_process_limit tuning fixes it — the problem is not process count. It is that Postfix ships with one smtp transport and one shared active queue, and that single lane is the whole bottleneck.
The fix is architectural: shard the queue by destination. Give the fast mailbox providers an aggressive, connection-cached lane, and quarantine the slow tarpitters into a throttled lane where a rate delay collapses them to concurrency 1. They crawl. Everyone else flies. Below is exactly how, including the two-layer config model that copy-paste tuning guides silently get wrong.
The single-transport trap
When qmgr schedules delivery it builds an in-memory active queue — the working set of messages it is trying to hand to delivery agents right now. That set is bounded by qmgr_message_active_limit (default 20000), and the number of concurrent delivery processes is capped by default_process_limit (default 100). Per destination, concurrency starts small and ramps: default_destination_concurrency_limit is 20, and each transport begins at initial_destination_concurrency (5), then grows.
Here is the mechanism that bites you. qmgr runs a concurrency feedback loop per destination: every delivery that succeeds nudges the window up by default_destination_concurrency_positive_feedback (1), and a connection refusal or 4xx deferral nudges it down, with default_destination_concurrency_failed_cohort_limit (1) triggering the back-off. So qmgr *does* notice a misbehaving domain and shrinks its window. What it does not do is isolate that domain. All destinations share the same active queue and the same pool of delivery slots. When Yahoo starts answering 421 4.7.0 [TSS04] ... temporarily deferred, those messages defer, land in the deferred queue, and their scheduled retries keep re-entering the active queue and eating slots your Gmail traffic wanted.
One transport means one shared scheduler. Split the transport and you get isolation almost for free: separate scheduler accounting, a separate concurrency window, and a separate rate budget per lane.
Diagnose before you shard
Do not tune blind. Find out which domains actually own your deferred queue before you build a lane for them.
# Which domains dominate the deferred queue, across age buckets?
qshape deferred
# Same, but sorted by total so the worst offender is on top
qshape -s deferredqshape prints a matrix: rows are domains, columns are age buckets in minutes (T 5 10 20 40 80 160 320 640 1280 1280+). A domain with a fat row far to the right is one whose mail is aging out — stuck for over an hour. That is your isolation candidate. A domain heavy only in the T/5 columns is transient and not worth a dedicated lane.
The second confirmation lives in the logs. Every delivery logs a delays= field with four numbers:
delays=0.1/1200/0.2/0.5Read them as before-qmgr / time-in-active-queue / connection-setup / transmission. The second number is the smoking gun. 1200 means the message sat in the active queue for twenty minutes waiting for a delivery slot — classic HOL blocking. Then name the offenders by their defer codes:
postqueue -p | tail -1 # total queued
grep -E ' (421|451|450) ' /var/log/mail.log | grep -Eo 'to=<[^>]+>' | sort | uniq -c | sort -rn | headThe signatures worth a dedicated slow lane, as of 2026: Yahoo 421 4.7.0 [TSS04] ... temporarily deferred, Microsoft 451 4.7.500 Server busy, and generic greylisters 450 4.2.0 ... try again later. Threshold rule of thumb: if a domain holds more than ~200 messages past the 320-minute bucket, or shows delays= second-field values consistently above 300 seconds, it earns its own throttled lane.
Carve transports in master.cf
Clone the smtp delivery agent into named services. In /etc/postfix/master.cf:
smtp-fast unix - - n - - smtp
-o syslog_name=postfix/fast
-o smtp_connection_cache_on_demand=yes
-o smtp_connection_reuse_time_limit=300s
-o smtp_mx_session_limit=2
smtp-slow unix - - n - - smtp
-o syslog_name=postfix/slow
-o smtp_connection_cache_on_demand=noEverything here is a legitimate smtp-agent override. syslog_name gives each lane its own log prefix so you can watch them independently. smtp_connection_cache_on_demand and smtp_connection_reuse_time_limit control connection reuse — on for the fast lane so you pipeline many messages down one held connection to Gmail, off for the slow lane where holding a connection open to a tarpitting server buys nothing. These belong in master.cf because they are properties of the delivery agent — the process that opens the SMTP connection.
What must not go here: concurrency and rate limits. That is the mistake that turns half the tuning blogs on the internet into no-ops.
Route with transport_maps
Build /etc/postfix/transport mapping domains to lanes:
gmail.com smtp-fast:
googlemail.com smtp-fast:
outlook.com smtp-fast:
hotmail.com smtp-fast:
yahoo.com smtp-slow:
aol.com smtp-slow:
.corp-example.com smtp-slow:The detail that separates a working config from a broken one: transport_maps supports exactly three left-hand key forms — user@domain, domain, and .domain (the leading dot matches the parent domain and all subdomains). **There is no * wildcard.** The catch-all is not a table entry at all; it is default_transport in main.cf. The trailing colon with an empty nexthop means "use this transport, resolve the destination normally via MX."
Build the map and hook it up:
postmap lmdb:/etc/postfix/transport# main.cf
transport_maps = lmdb:/etc/postfix/transport
default_transport = smtp-fastpostfix reload # reload, never restart, after a map or config changeBudget each lane
Here the two-layer model becomes concrete, and here most guides go silently wrong. Concurrency, rate delay, and recipient batching are not set with -o in master.cf. qmgr reads them from main.cf, keyed by the master.cf service name. The parameter pattern is <service>_destination_<knob>:
# main.cf — qmgr scheduler budgets, keyed by service name
smtp-fast_destination_concurrency_limit = 40
smtp-fast_destination_recipient_limit = 50
smtp-fast_initial_destination_concurrency = 10
smtp-slow_destination_concurrency_limit = 2
smtp-slow_destination_rate_delay = 3s
smtp-slow_destination_recipient_limit = 5
smtp-slow_initial_destination_concurrency = 1| Knob | Fast lane | Slow / penalty lane |
|---|---|---|
_destination_concurrency_limit | 40 | 2 |
_destination_rate_delay | 0 (unset) | 3s |
_destination_recipient_limit | 50 | 5 |
_initial_destination_concurrency | 10 | 1 |
| connection caching (master.cf) | on | off |
The critical side effect: a `_destination_rate_delay` greater than zero forces that transport's concurrency to 1, regardless of the concurrency limit you set. Postfix delivers one message at a time when a rate delay is active, then waits the delay between the end of one and the start of the next. Setting smtp-slow_destination_concurrency_limit = 2 alongside rate_delay = 3s is belt-and-suspenders — the rate delay wins, and you get serialized, one-every-three-seconds delivery. That is exactly right for a server that returns 451 Server busy the moment you open a second connection.
The fast lane keeps the default feedback loop, so if Gmail suddenly starts deferring, qmgr still backs its window off automatically — you get throttle-on-trouble without giving up the ceiling.
The penalty box
Combine three tiers and you have a self-defending outbound path:
- `default_transport = smtp-fast` — the sane default. Anything not named in the map gets the aggressive lane. Most of the internet delivers fine at high concurrency.
- `smtp-slow` for the known tarpitters you found with
qshape— Yahoo, AOL, that one corporate Exchange. - Promote and demote domains by editing
/etc/postfix/transport, runningpostmap lmdb:/etc/postfix/transport, thenpostfix reload. No restart, no dropped connections. When a domain that lived in the fast lane starts throwing421for a day, drop it intosmtp-slowfor the duration and pull it back out once the reputation event clears.
The rate-delay-forces-concurrency-1 behavior is the whole point of the penalty box: a freshly-detected offender is guaranteed to send one message at a time, spaced out, so it can never again grab a fistful of slots from your good traffic.
Verify it worked
Confirm both config layers actually resolved. postconf -P reads the effective per-service overrides from master.cf; postconf <param> reads main.cf:
# master.cf agent overrides resolved for the slow lane?
postconf -P | grep smtp-slow
# main.cf concurrency keying landed on the service name?
postconf smtp-slow_destination_concurrency_limit
# => smtp-slow_destination_concurrency_limit = 2
# read the deferred queue, worst offenders on top
qshape -s deferred | head
# watch each lane's real concurrency + delay independently
grep 'postfix/fast/smtp' /var/log/mail.log | grep -Eo 'delays=[^,]+' | tail
grep 'postfix/slow/smtp' /var/log/mail.log | grep -Eo 'delays=[^,]+' | tailThe split syslog_name prefixes (postfix/fast/smtp, postfix/slow/smtp) are what make this observable. Watch the fast stream: its delays= second field should stay near zero as concurrency climbs toward 40. Watch the slow stream: deliveries appear roughly every three seconds and the queue drains slowly but steadily — without ever touching the fast lane's numbers.
Checklist before you call it done:
- [ ] Identified the offending domains via
qshapeand thedelays=second field - [ ] Cloned
smtpinto named services inmaster.cf, each with a distinctsyslog_name - [ ] Put connection caching / MX-session limits on the agent in
master.cf - [ ] Put concurrency / rate / recipient limits in
main.cf, keyed by the service name - [ ] Built the transport map with real key forms (
domain,.domain) and setdefault_transport— no*wildcard
One operational reminder: the map is a lever you pull during incidents, not a set-and-forget file. Re-run qshape -s deferred weekly and move domains between lanes as sender reputation and receiver behavior drift.


