2026-10-26·by Sijie Wang#debug#keepalive#nat#smtp

appwrite-smtp-keepalive-debug

Appwrite SMTP lost mail: a reused connection with no probe

The symptom. Appwrite SMTP loses mail intermittently: some messages never arrive, with no steady pattern.

The log. The log showed sends failing, so the trace went to the source.

The agent reads the source. A coding agent read the Appwrite 1.9.9 source and followed it down to the mechanism.

The mechanism. The mails worker is a long-lived Swoole process. When a project has no custom SMTP, it takes the registered adapter ($register->get('smtp')), a singleton with keepAlive: true (app/init/registers.php). The messaging 1.2.0 SMTP adapter wraps PHPMailer 6.9.1 and, with keepAlive on, reuses the same PHPMailer instance across sends. Nothing probes the connection before reuse. PHPMailer only treats a connection as dead once a read has observed EOF, so a connection the server closed while idle still looks alive until the next send tries it.

The contrast. Project custom SMTP builds a fresh adapter per job, so it is not affected. Both paths set keepAlive: true; the difference is reuse, not the flag.

The finding. This mechanism has no probe: a reused connection is never checked for liveness before the next send, and there is no retry on that path. The trace stops here.

One more link: where do the 5 minutes come from? The 5 minutes are Mailpit's SMTP timeout: the deployment's SMTP server is Mailpit, and its SMTP idle timeout closes the session. NAT stays linked as the related concept: NAT mappings expire when idle (how-nat-works), and application heartbeats already double as NAT-mapping renewal (keepalive-two-layers). Idle timeouts are the same class of mechanism; this 5 minutes is Mailpit's.

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →