Bali, Indonesia
Insights

Why WooCommerce Checkout Hangs for Seconds When the SMTP Plugin Can't Connect

October 26, 2026
0:00
A customer clicks Place order, the spinner turns for ten, thirty, sometimes sixty seconds, and then the thank-you page shows up as if nothing happened. The order is fine. Payment went through. But nobody got an order confirmation email, and the delay happens on contact forms too. In most cases the shop is not slow. It is waiting for an SMTP connection that will never answer. This is a common ticket pattern on the support desk. It usually gets reported as "checkout is slow" or "the server is overloaded," so people start looking at CPU, PHP workers, or the database. The real cause sits one layer lower, in how a TCP connection behaves when a firewall silently ignores it.
WordPress sends mail through 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.
To open an SMTP connection, PHPMailer calls PHP's stream_socket_client(). Underneath, the kernel starts a TCP three-way handshake:
  1. The kernel sends a SYN packet to the SMTP host on port 587 (or 465, or 25) and moves the socket into the SYN-SENT state.
  2. 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's 220 greeting.
  3. If something in between blocks the port, the outcome depends on how it blocks.
There are two ways a firewall can block a port, and they feel very different to the application. A REJECT rule (or a closed port on a reachable host) sends something back, either a TCP RST or an ICMP "port unreachable." The kernel sees it, fails the connect with 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.
There is a second variant that looks identical from the browser, and it catches people who have confirmed the port is open. Port 465 uses implicit TLS. The client starts the TLS handshake immediately after TCP connects. Port 587 uses STARTTLS. The client connects in plain text, waits for the 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.
Start by taking the browser out of the picture. With WP-CLI, call 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' ) );"
If this takes about as long as the checkout hang and then prints 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-sent
Read the results like this. A quick "refused" means something is rejecting the connection, and the hang probably comes from somewhere else. A wait until the 5-second cap means packets are being dropped, and that matches the long checkout delay. A socket sitting in SYN-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."
Fixing the connectivity is the first step: use the port your mail provider supports and pair it with the matching encryption (465 with SSL/implicit TLS, 587 with STARTTLS). If outbound SMTP is blocked by your host, ask whether the port can be opened or which relay they expect you to use. On your own VPS, allow the destination in the outbound firewall. If you do want to block it, use REJECT rather than DROP so applications fail fast instead of hanging. That fixes today's outage, but the design is still fragile. Any future network issue at the SMTP provider puts the same wait back into checkout. There are a few ways to decouple the two. The most robust option on a server you control is a local mail relay. Point WordPress at the local MTA (Postfix or Exim, using the default 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 );
The late priority matters because the SMTP plugin also configures PHPMailer on 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.