Why the email step blocks the checkout request
wp_mail(), which wraps the PHPMailer library. An SMTP plugin tells PHPMailer to stop using the local mail() function and connect to a remote SMTP server instead, such as your mail host or a transactional email service.
The key detail is that this happens synchronously, inside the same PHP request that is processing the checkout. WooCommerce's checkout is an AJAX call. The browser posts the form, and PHP creates the order and changes its status. That status change fires the new-order and customer-processing emails. Each of those calls wp_mail(), and PHP will not return a response to the browser until those calls finish.
So the customer's spinner is tied directly to how long PHPMailer takes to talk to the SMTP server. When the server answers quickly, that is a fraction of a second and nobody notices. When it does not answer at all, the request waits until something gives up.
What the kernel does when the SMTP port is silently dropped
stream_socket_client(). Underneath, the kernel starts a TCP three-way handshake:
- The kernel sends a SYN packet to the SMTP host on port 587 (or 465, or 25) and moves the socket into the
SYN-SENTstate. - Normally the remote server replies with SYN-ACK, the kernel sends ACK, and the socket becomes
ESTABLISHED. Then the SMTP conversation starts with the server's220greeting. - If something in between blocks the port, the outcome depends on how it blocks.
ECONNREFUSED or a similar error, and PHPMailer reports a connection failure almost instantly. The checkout is barely delayed. The email just fails.
A DROP rule sends nothing. The SYN disappears. The kernel has no idea whether the packet was lost or the host is slow, so it does what TCP is designed to do: it retransmits the SYN with exponential backoff. On Linux the number of retries comes from net.ipv4.tcp_syn_retries. With the common default, the retries are spaced roughly 1, 2, 4, 8, 16, 32, and 64 seconds apart, which adds up to about two minutes before the kernel itself gives up with ETIMEDOUT.
In practice the application usually gives up first. stream_socket_client() accepts a timeout argument. PHP does a non-blocking connect and waits on the socket up to that timeout, and PHPMailer passes its own Timeout property there. The visible delay is therefore whichever is shorter: PHPMailer's timeout or the kernel's SYN retry window. PHPMailer's own default is long (300 seconds in the versions WordPress has bundled), so the kernel or the web server often ends things first. Many SMTP plugins set a shorter value.
Outbound DROP rules are common. Many hosts block outbound port 25 to stop spam from compromised sites, some block 465 and 587 too, and a VPS firewall with a default-deny outbound policy drops everything not explicitly allowed. That is why the same plugin settings work on one server and hang on another.
The other hang: the connection opens but nobody talks
220 greeting, sends EHLO, then upgrades with STARTTLS.
If the plugin is set to port 465 with "TLS/STARTTLS" encryption, PHPMailer connects and waits for a 220 greeting. The server, expecting a TLS ClientHello, waits for the client. Both sides are polite, both sides are silent, and nothing happens until a read timeout fires. TCP is fine here. The socket is ESTABLISHED, so nc shows the port as open. The deadlock is purely at the protocol level. The reverse mismatch (SSL on 587) usually fails faster, because the server reads the ClientHello as a garbage SMTP command and replies with an error, but how fast depends on the server.
One more wrinkle is that PHP's max_execution_time does not save you here. On Linux and other non-Windows systems, it counts the script's own execution time and leaves out time spent blocked in stream and socket operations. A 30-second limit will not cut off a socket that has been waiting for 60 seconds. What usually ends the wait is the web server or proxy timeout, or PHP-FPM's request_terminate_timeout, and in that case the customer may see a 502 or 504 even though the order was created.
Confirming email is the delay
wp_mail() directly and time it. This runs the same plugin configuration without placing an order.
BASH
# Run from the site's document root as the site's own user
time wp eval "var_dump( wp_mail( '[email protected]', 'SMTP timing test', 'test' ) );"bool(false), you have found the delay. If it returns true in under a second, the bottleneck is somewhere else.
Next, test the network path from the server itself, not from your laptop, because egress rules apply to the server.
BASH
# Try a TCP connect to the SMTP host; -w 5 caps the wait at 5 seconds
nc -vz -w 5 smtp.example.com 587
# While a checkout or the wp eval test is hanging, list sockets stuck in the handshake
ss -tn state syn-sentSYN-SENT toward the SMTP host's IP while checkout spins confirms it. If nc connects instantly but wp_mail() still hangs, check the port and encryption pairing from the previous section. openssl s_client -connect smtp.example.com:465 for implicit TLS, or openssl s_client -starttls smtp -connect smtp.example.com:587, shows whether the handshake actually completes.
Also check the plugin's own debug log or email log if it has one. Most will record the PHPMailer error string, and "Connection timed out" points to a different cause than "Connection refused."
Getting email out of the request path
mail() path or SMTP to 127.0.0.1) and configure that MTA to relay through your provider. PHP hands the message to a local process in milliseconds. The MTA queues it and retries in the background if the upstream is unreachable. The trade-off is that a stuck relay now shows up as a growing mail queue instead of a slow checkout, so someone has to watch it. Why Outbound Emails Queue Up When Your IP Is Not Blacklisted covers reading that queue.
On shared hosting without that option, look at deferring. WooCommerce has a woocommerce_defer_transactional_emails filter that queues order emails and sends them on PHP's shutdown hook instead of mid-checkout. Whether the browser gets its response before those sends depends on your WooCommerce version and how PHP is served, so test it with the timing methods above rather than assuming. Some SMTP plugins also offer background or queued sending. Check what yours does before relying on it.
The last option is a short timeout. It does not remove the wait, but it caps it. If your plugin has a timeout setting, use that. Otherwise, set it in a small must-use plugin:
PHP
<?php
// wp-content/mu-plugins/smtp-timeout.php
// Cap SMTP connect/read waits so an unreachable relay cannot stall checkout for minutes.
// ponytail: still blocks up to 10s per email; move to a local MTA relay or deferred sending to remove the wait.
add_action( 'phpmailer_init', function ( $phpmailer ) {
$phpmailer->Timeout = 10;
}, 99 );phpmailer_init, and some plugins set their own timeout there. Run the wp eval test again afterwards to confirm the cap actually applies.
If checkout hangs for a consistent number of seconds and order emails go missing, look at the network before the database. Time wp_mail() from the CLI, check for SYN-SENT sockets, and confirm the port and encryption pair. Once mail works again, plan to move sending off the customer's request path, because the next unreachable SMTP server will otherwise cause the same delay.