How Long Does an SMTP Session Stay Open? ConnectionTimeout 00:10:00 in Exchange and the Systems for Which That Is Too Short
Exchange terminates every incoming SMTP session after ten minutes, even if it is currently transferring data. Which senders stay on a connection that long, how to determine the actual session duration from the protocol log, and when ConnectionTimeout and ConnectionInactivityTimeout should be adjusted on a relay connector.
In short: An SMTP session has no natural end. RFC 5321 only limits the time spent waiting for the next step, and a client may continue delivering messages over an open connection for as long as the server keeps the connection open. By default, Exchange keeps it open for ten minutes on Receive Connectors, then the server terminates the connection regardless of whether data is currently flowing. This is irrelevant for Exchange-to-Exchange traffic and most MTAs because these senders reconnect on their own after seconds. For applications, gateways, and load generators that use a single connection for an entire sending run, however, this value causes interruptions that appear as connection errors on the client and as 421 4.4.1 Connection timed out in the Exchange protocol log.
Two timeouts with different meanings
A Receive Connector has two time limits that are often confused:
| Parameter | Meaning | Default Mailbox server | Default Edge Transport |
|---|---|---|---|
ConnectionInactivityTimeout | maximum idle time without client activity; the connection is then closed | 00:05:00 | 00:01:00 |
ConnectionTimeout | maximum total duration of the connection, even while it is actively transferring data | 00:10:00 | 00:05:00 |
Both values accept 1 second through 1 day (1.00:00:00), and ConnectionTimeout must be greater than ConnectionInactivityTimeout. The values apply per connector, separately for the internet-facing Default Frontend <Server>, the Transport service connector Default <Server> on port 2525, and every custom relay connector.
The idle timeout is unproblematic: Five minutes exactly matches the minimum time RFC 5321 requires a server to wait for the next command, and a client that sends nothing for five minutes has generally forgotten the connection itself. The total timeout is Exchange-specific: It starts counting when the connection is established and continues while the client submits message after message. After ten minutes, Exchange closes the connection wherever the dialog happens to be, if necessary in the middle of a DATA block.
There is no equivalent on the sending side: A Send Connector has only ConnectionInactivityTimeOut (ten minutes by default) and instead limits sessions through SmtpMaxMessagesPerConnection, 20 messages by default. As a client, Exchange therefore ends every connection after no more than 20 messages and creates a new one. This is why the total timeout is never noticeable between Exchange servers: Sessions last seconds.
What RFC 5321 requires
Section 4.5.3.2 of the standard defines minimum wait times a client should observe for each protocol step before giving up the connection:
| Step | Minimum client-side timeout |
|---|---|
Waiting for the 220 banner | 5 minutes |
Response to MAIL | 5 minutes |
Response to RCPT | 5 minutes |
Response to DATA (the 354) | 2 minutes |
| Sending a data block | 3 minutes |
| Response to the final period | 10 minutes |
| Server: waiting for the next command | at least 5 minutes |
The RFC does not specify an upper limit for the total duration of a session. A client that submits messages over the same connection for 30 minutes without remaining silent for more than a few seconds is standards-compliant. The final client value is notable: Ten minutes of waiting for the response after the final period, because the server accepts and takes over the message during this phase. If the client aborts too soon here, the message has already been delivered and will be delivered a second time on the next attempt. The same situation occurs in reverse if the server closes the connection at this point because of the total timeout.
If a server closes the connection with 421, the client should treat the transaction in progress under section 3.8 as though it had received a 451, meaning a temporary error requiring a retry. An MTA with a queue does exactly that. An application without a queue instead reports an exception and leaves the rest to its caller.
How long senders actually keep their sessions open
The client determines session duration, and the differences between sender types are substantial:
| Sender | Typical session duration | Limited by |
|---|---|---|
| Exchange Send Connector | Seconds | SmtpMaxMessagesPerConnection = 20 |
| Postfix with connection cache | maximum 5 minutes | smtp_connection_reuse_time_limit = 300s |
| Postfix without connection cache | one message per connection | Default behavior of the smtp client |
Application using .NET SmtpClient, JavaMail Transport, Python smtplib | as long as the object lives: the entire run for a batch job | only program code |
| Quarantine notifications from mail gateways | one session per notification run | Product behavior, partly with NOOP keepalive |
| Multifunction devices, scan-to-mail | one message per connection; several minutes for large scans over slow links | File size and bandwidth |
Load generators such as smtp-source -d | until the end of the run | Invocation parameters |
The first two rows explain why nobody notices this value for years in traditional environments: MTAs keep connections short on their own. For example, Postfix uses a cached connection for no more than five minutes before opening a new one, and Exchange disconnects after 20 messages. Both therefore stay below every Exchange default value.
The application row is the most common problem case. A batch job that sends invoices, pay stubs, or system notifications typically creates a client object, calls the send method on it in a loop, and closes it at the end. System.Net.Mail.SmtpClient has used the same connection for consecutive Send calls since .NET Framework 4 and sends QUIT only during Dispose; JavaMail behaves the same way with a Transport opened once. If the job runs longer than ten minutes, the 421 occurs somewhere in the middle, and the job terminates with an exception, in .NET for example with the text Service not available, closing transmission channel. The server response was: 4.4.1 Connection timed out. Which message is affected depends on runtime, making the error seem random: Sometimes it fails after 800 messages, sometimes after 1,200, depending on message size and server load.
The gateway row describes a documented case: Symantec (now Broadcom) Messaging Gateway sends spam quarantine notifications over a single connection and sends NOOP as a keepalive between messages. Exchange responds to NOOP with the five-second tarpit delay, so at most about 120 notifications can get through in ten minutes before the session ends with 421 4.4.1 and the gateway must reconnect.
The scanner row is a size problem rather than a volume problem: A 60 MB scan over a 2 Mbps connection requires around four minutes of transfer time alone; at 100 MB, it takes almost seven minutes. On an Edge Transport server with a five-minute total timeout, that is already enough to cause an interruption; on a Mailbox server, there is some margin left, but not much.
What happens when the connection is terminated
When the total timeout expires, Exchange writes the response 421 4.4.1 Connection timed out to the protocol log, sends it to the client, and closes the connection. For the transaction currently in progress: If the final period has not yet been sent, the message has not been accepted and must be repeated in full. If the period was sent and the connection is closed before the 250 response, the client has no information about whether Exchange took over the message; a properly implemented client retries it, and the recipient may receive it twice. The likelihood is small, but not zero with thousands of messages per run.
The proxy path also needs to be considered: The Front End Transport service accepts the connection on port 25 and forwards it as its own SMTP session to the Transport service on port 2525, where connector Default <Server> uses the same default values. A long session therefore appears in both logs, and an adjustment must include both connectors.
Determining the actual session duration from the protocol log
Before changing a value, it is worth looking at real sessions. The prerequisite is verbose protocol logging on the affected connector; it is already enabled on Default Frontend <Server>, but not on all other connectors:
Set-ReceiveConnector -Identity 'EX01\Relay Applikationen' -ProtocolLoggingLevel Verbose
The logs are located under %ExchangeInstallPath%TransportRoles\Logs\FrontEnd\ProtocolLog\SmtpReceive (Front End) and %ExchangeInstallPath%TransportRoles\Logs\Hub\ProtocolLog\SmtpReceive (Transport service), named by UTC hour as RECVyyyyMMddhh-nnnn.log. Each line is a protocol event with the fields date-time, connector-id, session-id, sequence-number, local-endpoint, remote-endpoint, event, data, and context. All lines in a session have the same session-id, so session duration is the difference between the first and last timestamp for that ID.
The following script evaluates the most recent log file of the day for a connector, summarizes lines per session, and displays the 15 longest sessions with the number of messages, duration, and whether Exchange ended them with 421 4.4.1:
$logPfad = Join-Path $env:ExchangeInstallPath 'TransportRoles\Logs\FrontEnd\ProtocolLog\SmtpReceive'
$connector = 'Relay Applikationen'
$tag = (Get-Date).ToUniversalTime().ToString('yyyyMMdd')
$datei = Get-ChildItem $logPfad -Filter "RECV$tag*.log" |
Sort-Object Name -Descending |
Select-Object -First 1
$sessions = @{}
Get-Content $datei.FullName |
Where-Object { $_ -notlike '#*' -and $_ -like "*$connector*" } |
ForEach-Object {
$c = $_ -split ','
$s = $c[2]
if (-not $sessions[$s]) {
$sessions[$s] = [pscustomobject]@{
IP = ($c[5] -split ':')[0]; Start = $c[0]; Ende = $c[0]
Zeilen = 0; Mails = 0; Timeout = $false
}
}
$sessions[$s].Ende = $c[0]
$sessions[$s].Zeilen++
if ($c[7] -like 'MAIL FROM*') { $sessions[$s].Mails++ }
if ($c[7] -like '421 4.4.1*') { $sessions[$s].Timeout = $true }
}
$sessions.Values |
Sort-Object Zeilen -Descending |
Select-Object -First 15 IP, Mails, Zeilen, Timeout,
@{ n = 'Dauer_s'
e = { [math]::Round(([datetime]$_.Ende - [datetime]$_.Start).TotalSeconds, 1) } } |
Format-Table -AutoSize
In the output, you can identify affected systems by Timeout being set to True and Dauer_s being close to 600: The session lived exactly as long as the connector permits. Sessions with many messages and a duration well below 600 seconds are unproblematic, even if they are currently the longest ones. To see which sources are affected, grouping the marked sessions is sufficient:
$sessions.Values |
Where-Object { $_.Timeout } |
Group-Object IP |
Select-Object Name, Count |
Sort-Object Count -Descending
Two limitations of this approach: A session that crosses an hour boundary is split across two log files and appears shortened in an individual file; for a daily analysis, read all files for the day. Also, Mails counts MAIL FROM commands, meaning attempts rather than accepted messages.
Adjusting values: on which connector and by how much
The default values protect the internet-facing connector, where arbitrary remote systems can occupy connections. They remain unchanged there; a legitimate external MTA reconnects anyway. Adjust the dedicated connector through which the identified internal systems submit messages. If no such connector exists, create one restricted to the sender IPs with RemoteIPRanges; that is better than increasing the value on Default Frontend. To view the current state of all connectors:
Get-ReceiveConnector |
Format-Table Name, TransportRole, ConnectionTimeout, ConnectionInactivityTimeout, TarpitInterval -AutoSize
The adjustment itself, here with a total duration of one hour and an unchanged idle timeout:
$werte = @{
ConnectionTimeout = '01:00:00'
ConnectionInactivityTimeout = '00:05:00'
}
Set-ReceiveConnector -Identity 'EX01\Relay Applikationen' @werte
Set-ReceiveConnector -Identity 'EX01\Default EX01' @werte
The value should exceed the longest legitimate session shown by the analysis, with headroom for load spikes. One hour covers most batch runs; a two-hour overnight run accordingly requires more, up to the maximum of one day. However, the value should not be arbitrarily high even on an internal connector, because MaxInboundConnectionPerSource (default 20) and MaxInboundConnection (default 5000) also count: A client that repeatedly opens new connections in addition to a stuck connection reaches the per-source limit sooner the longer old connections remain open.
For senders that send NOOP between messages, set TarpitInterval on the same connector to 00:00:00. The tarpit delay provides no benefit for authenticated or IP-restricted internal senders and artificially extends every session.
The Exchange-side change fixes the symptom. The more reliable solution is in the client: It reconnects after a fixed number of messages, as Exchange does after 20 and Postfix does after five minutes. With .NET SmtpClient, this means creating and discarding the object for blocks of, for example, 100 messages; with JavaMail, the Transport is closed and reopened accordingly. This allows sending to targets whose timeouts cannot be adjusted, especially Exchange Online, whose inbound connectors have no timeout parameters.
Additional time limits on the path
The Exchange value is not the only limit. Firewalls and load balancers maintain their own idle timers for TCP connections: A FastL4 profile on an F5 BIG-IP defaults to 300 seconds, and Azure Load Balancer to four minutes. These timers measure idle time rather than total duration and therefore take effect during sending pauses, for example when a batch job reads data from the database between two blocks. The smallest value along the entire path always applies. The article F5 BIG-IP as an outbound proxy for bulk email delivery explains how to size timeouts on a load balancer for persistent SMTP connections.
Comments
Comments are loaded from GitHub / Giscus.