Current Behavior
Forums using smtp mail driver against AWS SES (and likely other SMTP relays with sub-100s idle timeouts) periodically log:
flarum.ERROR: Failed to send email. {"recipient_email":"...","reason":"Expected response code \"250\" but got code \"451\", with message \"451 4.4.2 Timeout waiting for data from client.\".","exception_class":"Symfony\\Component\\Mailer\\Exception\\UnexpectedResponseException"}
The pattern on a forum that recycles its queue worker every 10 minutes is striking — failures cluster late in each worker's life, never near the start:
| Failure |
Worker spawned |
Worker exited |
Failure age within worker |
| 13:41:53 |
13:32:39 |
13:42:42 |
T+9:14 |
| 16:28:43 |
16:20:42 |
16:30:44 |
T+8:01 |
That shape rules out network blips and points at connection-age dependence inside a single worker.
Root Cause
Symfony\Component\Mailer\Transport\Smtp\SmtpTransport::$pingThreshold defaults to 100 seconds. The transport reuses the SMTP connection across sends within a process; before each send it only checks the connection (NOOP) if the inter-message gap exceeds the threshold:
https://github.com/symfony/symfony/blob/7.x/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php
SES closes idle connections faster than 100s (operationally observed in the 30–90s range — AWS doesn't publish an exact number). So when emails are spaced 30–100s apart, SES has already torn down the connection but the transport skips the NOOP and writes DATA + body into the dead pipe. SES eventually responds with 451 4.4.2 Timeout waiting for data from client to whatever zombie state remains.
Flarum\Mail\SmtpDriver::buildTransport() builds the transport via the factory and returns it without configuring the ping threshold:
https://github.com/flarum/framework/blob/main/framework/core/src/Mail/SmtpDriver.php
Steps to Reproduce
- Configure SMTP against AWS SES (
email-smtp.<region>.amazonaws.com:587, STARTTLS).
- Run a long-lived
queue:work worker with a non-trivial trickle of email-bound notifications (mentions / replies / private discussions / etc.).
- After enough idle gaps in the 30–90s range, observe
451 4.4.2 Timeout waiting for data from client in storage/logs/flarum-YYYY-MM-DD.log.
Expected Behavior
The mailer should detect a closed connection before attempting the next send and reconnect transparently. Symfony exposes SmtpTransport::setPingThreshold(int $seconds) for exactly this; the default just isn't tuned for SES.
Suggested Fix
In framework/core/src/Mail/SmtpDriver::buildTransport(), after building the transport, lower the ping threshold:
$transport = $this->factory->create(new Dsn(...));
if ($transport instanceof SmtpTransport) {
$transport->setPingThreshold(20);
}
return $transport;
20s is well under any reasonable SMTP idle timeout but rare enough that NOOP overhead is negligible. Could also be made configurable via a setting (mail_smtp_ping_threshold) for forums that need to tune it.
Happy to send a PR.
Environment
- Flarum core: 2.0.0-rc.1
- PHP: 8.4.20
- Mail driver: smtp → AWS SES (eu-central-1)
- Confirmed default is 100s in current
main branch of flarum/framework.
Possible Solution
See "Suggested Fix" above. Has been applied as a local vendor/ patch on our forum and is being observed; will report back if the failure rate doesn't drop to zero.
Additional Context
This issue likely affects everyone using the smtp driver against SES, but is invisible until the queue worker is long-lived enough for the cumulative failure rate to be noticed in logs (we only saw it because we set up a daily error-tally cron). On installs that send via the synchronous "sync" queue driver, it would surface as occasional bounced notifications without an obvious pattern.
Current Behavior
Forums using
smtpmail driver against AWS SES (and likely other SMTP relays with sub-100s idle timeouts) periodically log:The pattern on a forum that recycles its queue worker every 10 minutes is striking — failures cluster late in each worker's life, never near the start:
That shape rules out network blips and points at connection-age dependence inside a single worker.
Root Cause
Symfony\Component\Mailer\Transport\Smtp\SmtpTransport::$pingThresholddefaults to 100 seconds. The transport reuses the SMTP connection across sends within a process; before each send it only checks the connection (NOOP) if the inter-message gap exceeds the threshold:https://github.com/symfony/symfony/blob/7.x/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php
SES closes idle connections faster than 100s (operationally observed in the 30–90s range — AWS doesn't publish an exact number). So when emails are spaced 30–100s apart, SES has already torn down the connection but the transport skips the NOOP and writes DATA + body into the dead pipe. SES eventually responds with
451 4.4.2 Timeout waiting for data from clientto whatever zombie state remains.Flarum\Mail\SmtpDriver::buildTransport()builds the transport via the factory and returns it without configuring the ping threshold:https://github.com/flarum/framework/blob/main/framework/core/src/Mail/SmtpDriver.php
Steps to Reproduce
email-smtp.<region>.amazonaws.com:587, STARTTLS).queue:workworker with a non-trivial trickle of email-bound notifications (mentions / replies / private discussions / etc.).451 4.4.2 Timeout waiting for data from clientinstorage/logs/flarum-YYYY-MM-DD.log.Expected Behavior
The mailer should detect a closed connection before attempting the next send and reconnect transparently. Symfony exposes
SmtpTransport::setPingThreshold(int $seconds)for exactly this; the default just isn't tuned for SES.Suggested Fix
In
framework/core/src/Mail/SmtpDriver::buildTransport(), after building the transport, lower the ping threshold:20s is well under any reasonable SMTP idle timeout but rare enough that NOOP overhead is negligible. Could also be made configurable via a setting (
mail_smtp_ping_threshold) for forums that need to tune it.Happy to send a PR.
Environment
mainbranch offlarum/framework.Possible Solution
See "Suggested Fix" above. Has been applied as a local vendor/ patch on our forum and is being observed; will report back if the failure rate doesn't drop to zero.
Additional Context
This issue likely affects everyone using the
smtpdriver against SES, but is invisible until the queue worker is long-lived enough for the cumulative failure rate to be noticed in logs (we only saw it because we set up a daily error-tally cron). On installs that send via the synchronous "sync" queue driver, it would surface as occasional bounced notifications without an obvious pattern.