Postfix does not open unlimited outbound connections to a single destination. The queue manager (qmgr) enforces a per-destination concurrency cap controlling how many simultaneous delivery attempts may target the same recipient domain at once. This parameter, smtp_destination_concurrency_limit, is behind two common operational failures: queue gridlock from one slow destination monopolizing active queue slots, and reputation penalties from hammering a fragile destination that then throttles or blocklists your IP.

The default works for general-purpose MTAs sending moderate volume to healthy destinations. But with rate-limited providers, corporate servers with strict connection caps, or a backlog that needs draining fast, the default can be either too aggressive or too conservative. Since Postfix 2.5, the concurrency limit is a ceiling that an adaptive feedback algorithm approaches and retreats from based on delivery outcomes, similar to TCP congestion control. Understanding both the limit and the feedback algorithm is necessary before making changes.

What it controls

smtp_destination_concurrency_limit (default: $default_destination_concurrency_limit, which defaults to 20) controls the maximum number of simultaneous outbound SMTP deliveries to a single destination. “Destination” here means a resolved next-hop address: typically the MX host of the recipient domain, or a relayhost if one is configured.

When mail for a destination enters the active queue, qmgr opens delivery connections up to the concurrency limit. If 500 messages are queued for example.com and the limit is 20, Postfix opens at most 20 simultaneous connections to example.com’s MX hosts. The remaining messages wait in the active queue until a slot frees.

Key facts:

  • This is a per-destination limit, not a global limit. Postfix will open up to 20 connections to gmail.com and simultaneously up to 20 to outlook.com, and so on for every destination with queued mail.
  • There is no built-in global concurrency cap across all destinations. The global process pool is governed by default_process_limit (default: 100), which caps total spawned smtp delivery agents across all destinations. If you need a hard cap on total outbound connections, tune default_process_limit or use a dedicated relay transport.
  • The limit applies per transport. smtp_destination_concurrency_limit governs the smtp transport (outbound SMTP delivery). The local, virtual, and pipe transports each have their own *_destination_concurrency_limit parameters.

If the limit is too high for a destination’s tolerance, the remote MTA may respond with 4xx deferrals, greylisting, or outright connection throttling. Sustained aggressive concurrency can trigger reputation penalties or blocklistings. If the limit is too low, you cannot drain a backlog fast enough. Mail piles up in the active and deferred queues even though destinations are healthy and willing to accept more.

Adaptive concurrency feedback

Since Postfix 2.5, the per-destination concurrency limit is not a static number. It is a ceiling that an adaptive feedback algorithm approaches and retreats from based on delivery outcomes.

The algorithm operates on pseudo-cohorts: groups of messages sent to the same destination in a burst. The lifecycle:

  1. Initial concurrency: Postfix starts with initial_destination_concurrency (default: 5). The first pseudo-cohort to a destination opens at most 5 simultaneous connections.
  2. Positive feedback: When deliveries in a cohort succeed without connection or handshake failures, Postfix increments the effective concurrency toward the configured ceiling. The rate is controlled by default_destination_concurrency_positive_feedback (default: 1).
  3. Negative feedback: When a delivery fails with a connection or handshake failure (a network-level failure, not a 4xx SMTP response), Postfix decrements concurrency. The amount is controlled by default_destination_concurrency_negative_feedback (default: 1).
  4. Failed cohort limit: If an entire pseudo-cohort fails, Postfix counts it. After default_destination_concurrency_failed_cohort_limit (default: 1) failed pseudo-cohorts, Postfix declares the destination “dead” and suspends delivery to it until the next queue scan.
flowchart TD
    A[Mail enters active queue for destination] --> B[Start at initial_destination_concurrency]
    B --> C{Delivery outcome}
    C -->|Success| D[Positive feedback: grow concurrency]
    C -->|Connection or handshake failure| E[Negative feedback: reduce concurrency]
    D --> F{Concurrency < smtp_destination_concurrency_limit?}
    F -->|Yes| C
    F -->|No| G[Hold at ceiling]
    E --> H{Cohort fully failed?}
    H -->|No| C
    H -->|Yes| I[Increment failed cohort counter]
    I --> J{Counter >= failed_cohort_limit?}
    J -->|Yes| K[Declare destination dead, suspend delivery]
    J -->|No| C
    G --> C
    K --> L[Retry on next queue scan]

The defaults for positive_feedback (1), negative_feedback (1), and failed_cohort_limit (1) are documented as reproducing pre-2.5 Postfix behavior. You do not need to change these unless you have a specific reason.

Critical distinction: 4xx SMTP deferrals are not treated as connection or handshake failures by the concurrency algorithm. A 4xx deferral moves the message to the deferred queue with exponential backoff, but it does not trigger negative feedback on the concurrency window. Only network-level failures (connection refused, connection timed out, TLS handshake failure) affect the adaptive concurrency.

Where it shows up in production

The most common scenario is queue gridlock: a single slow or failing destination consumes a disproportionate share of active queue slots.

The active queue is capped at qmgr_message_active_limit (default: 20,000). When mail for one destination fills the active queue and that destination is slow to accept deliveries, all other destinations are starved. The queue manager uses fair scheduling across destinations but does not deprioritize a destination consuming slots without delivering.

Diagnostic pattern:

  • Active queue is near qmgr_message_active_limit
  • Deferred queue is growing steadily
  • One destination dominates deferred entries with “connection timed out” or rate-limit deferrals
  • Overall delivery rate is flat or declining despite queue depth
  • System CPU and network utilization are low (the constraint is the destination, not your hardware)

In this state, reducing the concurrency limit for the problem destination frees active queue slots for healthy destinations. The TUNING_README warns: “Knee-jerk changes to these parameters in the face of congestion can actually make problems worse.” The right approach is a targeted per-transport override, not a global limit reduction.

The second scenario is the opposite: a backlog of valid mail to a healthy destination where the default concurrency of 20 is too low to drain it. A dedicated transport with a higher concurrency limit can help, but verify the destination can tolerate the load before raising it.

Per-transport overrides

Dedicated transports for fragile destinations

The recommended pattern for destinations with strict rate limits or fragile infrastructure is a dedicated transport in master.cf with an overridden concurrency limit in main.cf.

Step 1: Define the transport in master.cf. The chroot field (5th column) must match your existing smtp service definition:

slow    unix  -       -       n       -       -       smtp
    -o smtp_connect_timeout=5

Step 2: Set the per-transport parameters in main.cf using the transport name as a prefix:

postconf -e 'slow_destination_concurrency_limit=2'
postconf -e 'slow_destination_rate_delay=1s'
postconf -e 'slow_destination_recipient_limit=10'

Step 3: Route specific domains to this transport. Verify your transport map path first; the transport_maps parameter in main.cf must point to this file:

# Confirm the transport map is configured
postconf transport_maps

# Check existing entries before appending
grep fragile-provider.com /etc/postfix/transport

# Add the routing entry
echo 'fragile-provider.com    slow:' >> /etc/postfix/transport
postmap /etc/postfix/transport
postfix reload

The common mistake: operators add the transport in master.cf but forget the corresponding *_destination_concurrency_limit in main.cf. Without it, the transport inherits the default concurrency of 20, defeating the purpose.

smtp_connect_timeout per-transport override

smtp_connect_timeout does not have a per-transport name parameter in main.cf. It must be overridden with -o directly in the master.cf service definition. The timeout applies to TCP connection establishment before any SMTP protocol exchange begins.

destination_rate_delay interaction

When you set destination_rate_delay (which pauses between deliveries to the same destination) alongside a reduced concurrency limit, the failed_cohort_limit parameter becomes critical. With rate delay, a pseudo-cohort takes longer to complete, and the default of 1 may cause destinations to be declared dead too aggressively. The TUNING_README recommends increasing failed_cohort_limit when using destination_rate_delay. Consider raising it to 2 or 3.

Raising the global default

The TUNING_README notes that the default of 20 “seems enough to noticeably load a system without bringing it to its knees.” For most deployments this is correct. If you need higher throughput to healthy destinations, prefer dedicated transports with elevated concurrency rather than raising the global default. A global increase affects all destinations including fragile ones that may already be on the edge of throttling you.

Signals to watch

SignalWhy it mattersWarning sign
Active queue size vs qmgr_message_active_limitApproaching the limit blocks new deliveries from being scheduled regardless of destination healthRatio above 80% sustained for over 10 minutes
Deferred queue growth rateGrowing deferred queue with a single dominant destination indicates the feedback loop is not keeping upSustained positive growth over 4 hours
Per-destination deferral reason codes“4.7.1 rate limited” or “connection timed out” for one destination while others succeed means concurrency is too high for that destinationOne destination accounting for over 50% of deferrals
Per-destination delivery rateA destination whose delivery rate drops while others stay flat is being throttled or is failingSudden drop for one destination with flat or rising queue depth
SMTP connection latency to specific destinationsElevated connect time indicates network path issues or destination-side throttling before it shows in deferralsConnection establishment consistently over 10 seconds
smtp process count vs default_process_limitThe global process pool can starve high-concurrency destinations if total smtp agents hit the capProcess count sustained above 80% of limit

Monitoring with Netdata

Netdata’s Postfix collector surfaces queue depth and mail flow signals that reveal concurrency-related problems before they become incidents.

  • Per-second active and deferred queue depth: the ratio between them distinguishes a destination problem (deferred growing, active near limit) from a global problem (both growing uniformly).
  • Mail flow velocity correlation: the injected-vs-delivered rate alongside queue depth shows whether the system is draining or accumulating. A widening gap between injection and delivery rates with a growing active queue is the classic gridlock precursor.
  • Deferred queue growth rate: Netdata computes rates per second, so you see the derivative of queue depth, not just the absolute value. A positive slope sustained over minutes is actionable long before any static threshold is crossed.
  • System-level signals: CPU utilization, network connections, and process counts alongside Postfix metrics distinguish “destination is slow” (low CPU, low network) from “we are the bottleneck” (high CPU, process exhaustion).
  • Anomaly detection: ML-based anomaly flags on queue depth and delivery rate catch early divergence from baseline that precedes gridlock, even when absolute values are still within nominal ranges.

For deeper context on how Postfix’s queue architecture creates the failure patterns this parameter controls, see How Postfix actually works in production: a mental model for operators.