The symptom: customers do not receive order emails
A customer places an order in your WooCommerce store, pays, and never gets a confirmation. The merchant notification also fails to arrive. The order is visible in the admin, money moved through the payment gateway, but the inbox stays empty on both sides. Sometimes one email type works (password resets) while another does not (order confirmations). Sometimes the mail log plugin shows the send as successful, yet nothing lands in Gmail.
What this symptom actually means
WooCommerce does not have its own email-sending stack. Every transactional message, from the "Thank you for your order" to the admin "New order" notification, is built by a class that extends WC_Email and is ultimately handed off to WordPress's wp_mail() function. The WooCommerce Email FAQ states this directly: "WooCommerce triggers the wp_mail() function. This function signals WordPress to process the email. However, since WordPress isn't an email server, it delegates this task to PHP."
That means a missing WooCommerce order email is really one of four failure modes, and you have to diagnose which one before you can fix it:
- WooCommerce never triggers the email. The order never reached the status that fires the trigger (the most common real cause is an order stuck in "Pending payment"), the email is disabled in settings, or a plugin conflict blocks the hook chain.
- WooCommerce triggers the email but
wp_mail()is never actually called. A background email queue is enabled, WP-Cron is broken, and the queued action is silently marked "completed" without ever dispatching the mail. Or an HPOS-incompatible plugin hooks into the old order-save path and never fires on modern orders. wp_mail()is called and returns successfully, but the host never delivers the message. This is a WordPress-level problem, not a WooCommerce-level one. Every fix here is identical to why WordPress is not sending email and how to fix it.- The message leaves the host but fails authentication at the recipient. SPF, DKIM or DMARC rejects it. Also a WordPress-level concern.
If you skip the first two layers and go straight to "it must be SMTP", you will waste hours on a problem that was an unchecked checkbox or a pending-payment order. Work through them in order.
Common causes, ordered by likelihood
On a store that was sending emails fine until recently, the real order is usually:
- The order is stuck in Pending payment. This is by design. No customer confirmation is sent for pending orders. It is the single most reported non-bug.
- The payment gateway's IPN or webhook never reached the site, so the order never transitioned to "Processing", so the "Customer Processing Order" email never fired.
wp_mail()itself is broken at the WordPress or hosting level, affecting WooCommerce only incidentally.- HPOS was enabled and an older email-related plugin hooks
save_post_shop_order, which HPOS orders no longer trigger. - The background email queue is on, WP-Cron is broken on the server, and queued messages are silently marked done.
- The "Enable this email notification" checkbox got unchecked by someone editing settings, or during a plugin/theme update that touched email options.
- WooCommerce 9.5 added a new customer-facing failed-order email. After upgrading to 9.5 some stores saw new emails appearing that they did not expect, reconfigured things, and accidentally disabled legitimate ones in the process.
Diagnosis: figure out which layer is failing
Run these checks in order. Each is non-destructive.
Step 1: check the order status before anything else
Open WooCommerce > Orders and find the affected order. Look at the Status column.
- Processing: the gateway confirmed payment, WooCommerce should have fired both the customer "Order Processing" email and the merchant "New Order" email. Continue to step 2.
- Completed: same as above, plus the "Customer Completed Order" email should have fired. Continue to step 2.
- Failed: from WooCommerce 9.5 onward, both the merchant and the customer should receive a "Failed Order" notification. Continue to step 2.
- Pending payment: stop. This is the designed behavior. The WooCommerce Email FAQ says explicitly: "If your new orders have a Pending Payment status, no email will be sent." A pending order means the gateway never confirmed payment. Jump to the payment gateway section below.
- On hold: the customer should have received an "Order On-Hold" email. Check step 2 for that specific email type only.
You will know the diagnosis is complete when: you can name the exact status the order is in, and you know which email the hooks should have fired based on that status.
Step 2: check that the email is actually enabled in WooCommerce settings
Go to WooCommerce > Settings > Emails. You will see a table of every registered email type: New order, Cancelled order, Failed order, Order on-hold, Processing order, Completed order, Refunded order, Customer invoice, Customer note, Reset password, New account. Each row has a manage button.
Click the one you expect to fire (for a customer confirmation of a paid order, that is Processing order). Confirm:
- The Enable/Disable checkbox at the top is checked.
- The Recipient(s) field is correct (for customer emails this is left blank, because WooCommerce uses the order's billing email; for admin emails this must contain a valid address, usually the shop owner).
- The From name and address in Settings > Emails > Email sender options are real and are on a domain you actually send from.
You will know it worked when: every affected email shows "Enabled" in the manage view, and the recipient fields look right. Save the form even if you did not change anything. I have seen stores where the checkbox looked enabled but the option was not actually persisted in the database until a re-save.
Step 3: send a manual test and capture it with a mail logger
Install Check & Log Email if you do not already have it. You need a record of every wp_mail() call that leaves WordPress, whether WooCommerce triggered it or not.
Now trigger the actual email from a known-good state. The cleanest way to do this without creating a real test order is WooCommerce's own "Resend" action:
- Open WooCommerce > Orders, click an affected order.
- In the Order actions box on the right, pick Resend new order notification (or the relevant email type) from the dropdown and click the update/send button.
- Check the Check & Log Email log.
Three outcomes tell you which layer is failing:
- The log shows a row with status "Sent" and the message actually arrives in the inbox. The WooCommerce-side trigger, the WordPress-side transport, and the receiving-server authentication all work. The original missing email was either a one-off or tied to a status transition that never happened.
- The log shows a row with status "Sent" but nothing arrives in the inbox.
wp_mail()was called and returned without an exception, but the host or the receiver discarded the message. This is a transport or deliverability problem, not a WooCommerce problem. See why WordPress is not sending email to diagnose the WordPress/hosting/DNS layer. - The log is empty. WooCommerce did trigger the email (the "Resend" action is unconditional) but
wp_mail()itself was never reached. Something is intercepting the call before it leaves WooCommerce. This points at HPOS, the background queue, or a plugin that filterswoocommerce_mail_callbackaway.
You will know this check is complete when: you can point to one of those three outcomes. That outcome tells you which section below to apply.
Solutions by cause
When orders stay "Pending" and no email is sent
This is the most common non-bug in WooCommerce email tickets. The "Customer Processing Order" email fires on the woocommerce_order_status_pending_to_processing_notification hook, which only runs when an order transitions from Pending to Processing. That transition is performed by the payment gateway after it confirms the payment, not by WooCommerce itself.
Typical reasons an order stays in Pending:
- The customer closed the browser tab before completing the payment redirect (PayPal, iDEAL, Stripe Checkout).
- The card was declined but the gateway did not update the order to Failed.
- The gateway's IPN (PayPal) or webhook (Stripe, Mollie, Adyen) failed to reach your site. Common reasons: a firewall rule, HTTP Basic Auth protecting the site, an incorrect callback URL in the gateway dashboard, or the site being behind a staging password.
- The gateway timed out or the site briefly 500'd during the callback.
To fix it:
- Open the gateway's dashboard (Stripe, Mollie, PayPal, Adyen, etc.) and look at the recent transactions or webhooks log. You are looking for failed webhook deliveries pointing at your site. The gateway's log will quote the exact HTTP status it got back.
- If the gateway reports the webhook failed: fix the reachability problem (remove basic auth, whitelist the gateway IP ranges in your firewall, correct the callback URL).
- Once the callback path works again, the gateway dashboard will usually let you "retry" or "resend" the webhook for the stuck orders. When WooCommerce receives it, the order transitions to Processing and the customer confirmation fires automatically.
- For orders where the customer abandoned the payment flow on purpose, do not send a "fake" confirmation. Instead, set WooCommerce's Hold Stock (minutes) in Settings > Products > Inventory so abandoned pending orders auto-cancel after a reasonable window (60 to 120 minutes works for most stores).
You will know it worked when: the gateway dashboard shows no failed webhooks for recent orders, new paid orders land in "Processing" automatically, and the customer confirmation arrives in the inbox without manual intervention.
When the email is disabled, the recipient is wrong, or the From address is unreachable
This sounds too basic to be the cause, but it genuinely is the cause for about one ticket in five. A theme update, a plugin conflict, or someone "cleaning up" settings can leave an email type disabled. And the default From address WooCommerce ships with is wordpress@<server hostname>, which fails DMARC against any reasonable inbox provider.
- Go to WooCommerce > Settings > Emails. Enable every email type you need.
- For the admin emails (New order, Cancelled order, Failed order), set a real, monitored address as the recipient. Not a distribution list that might silently bounce.
- At the bottom, open Email sender options and set "From" name and "From" address to a mailbox you actually control on your site's domain. Not
wordpress@yoursite.nl, not your personal Gmail. If you do not have a mailbox on your sending domain yet, createorders@yoursite.nlornoreply@yoursite.nlfirst.
You will know it worked when: the Email sender options From address is on the same domain as your site, every email type you need shows "Enabled", and a resend test (step 3 of diagnosis) produces a Check & Log Email row with the new From address.
The HPOS migration trap: legacy plugins miss new orders
High Performance Order Storage (HPOS) replaces the old wp_posts/wp_postmeta storage for WooCommerce orders with dedicated tables (_wc_orders, _wc_order_addresses, _wc_order_operational_data, _wc_orders_meta). It was first offered as an opt-in feature in WooCommerce 7.1 (November 2022), and marked stable and enabled by default for new installs only in WooCommerce 8.2 (October 2023). Existing stores are not auto-migrated. They have to opt in.
That history matters for email debugging because the failure mode is specific: plugins that hook into save_post or save_post_shop_order (a pattern common in older email-log and notification plugins) receive no signal when an HPOS order is saved. The plugin silently stops working. Symptoms on a store that just enabled HPOS:
- Orders still go through. The checkout still works. The admin still lists the orders.
- The email log plugin stops recording anything from WooCommerce after the HPOS cutover date.
- Custom notification plugins that read order data with
get_post_meta()directly return stale or empty data. - The Email Log plugin's "last send" timestamp sits frozen at the day before HPOS was enabled.
To fix it:
- Open WooCommerce > Settings > Advanced > Features and look at the HPOS row. It tells you whether HPOS is active and, crucially, lists "incompatible plugins" that are blocking or not declaring support.
- For every plugin in that list, check the plugin's changelog or repository for an HPOS compatibility release. The official guidance is to declare compatibility via
\Automattic\WooCommerce\Utilities\FeaturesUtil::declare_compatibility()and use CRUD methods ($order->get_meta(),$order->update_meta_data()) instead ofget_post_meta(). - Update every plugin to the version that declares HPOS compatibility. If a plugin has not been updated and is abandoned, replace it. FluentSMTP, WP Mail SMTP, Post SMTP and Check & Log Email have all supported HPOS for a long time.
- If you must run an HPOS-incompatible email plugin temporarily, you can keep WooCommerce in "compatibility mode" where both the old posts table and the new HPOS tables are kept in sync. This is slower and is a stopgap, not a permanent solution. Disable the "Compatibility mode" option only after every email-related plugin is known-compatible.
You will know it worked when: WooCommerce > Settings > Advanced > Features shows no incompatible plugins, and a fresh test order triggers a log entry in both Check & Log Email and your notification plugin.
The background email queue silently drops messages
WooCommerce ships with the woocommerce_defer_transactional_emails filter. When a plugin or snippet returns true from it, WooCommerce stops sending transactional emails inline during the request and pushes them to WC_Background_Emailer, which uses WordPress Action Scheduler (and therefore WP-Cron) to dispatch them on a later request. The tradeoff: the checkout page returns faster, but if WP-Cron is broken on the server, the queued jobs are silently marked completed without ever dispatching the mail. No exception is thrown, no error is logged. The queue shows the action as "completed" and the customer gets nothing.
To check whether you are affected:
- Go to WooCommerce > Status > Scheduled Actions and filter by the
woocommerce_send_queued_transactional_emailhook. Recent actions should show as "Complete" with a recent timestamp. - If the queue has many Pending actions that never move to Complete, Action Scheduler itself is stuck, usually because WP-Cron is broken. Check WP-Cron by visiting
yoursite.nl/wp-cron.php?doing_wp_cronin a browser; it should return a near-blank page within a second or two. - If WP-Cron is broken, disable the broken in-HTTP cron and set up a real server cron hitting
wp-cron.phpevery minute. First, openwp-config.phpin your hosting panel's file manager (or download it via SFTP) and add this line above the/* That's all, stop editing! */comment:
define( 'DISABLE_WP_CRON', true );
Then add a cron job in your hosting panel's cron scheduler (usually under "Scheduled Tasks" or "Cron Jobs" in cPanel/Plesk) that runs every minute with the command:
wget -q -O - https://yoursite.nl/wp-cron.php?doing_wp_cron >/dev/null 2>&1
If you have SSH access, you can add this directly to your crontab instead:
* * * * * wget -q -O - https://yoursite.nl/wp-cron.php?doing_wp_cron >/dev/null 2>&1
If you cannot fix WP-Cron immediately, the quickest workaround is to stop deferring transactional mail entirely, so WooCommerce sends synchronously again. Create a file called disable-wc-email-defer.php in wp-content/mu-plugins/ using your hosting panel's file manager or SFTP client, with this content:
<?php
// wp-content/mu-plugins/disable-wc-email-defer.php
add_filter( 'woocommerce_defer_transactional_emails', '__return_false' );
This brings the old behavior back: emails send during the checkout request, at the cost of a slightly slower thank-you page. On a low-volume store this is fine. On a busy store, fix WP-Cron instead and keep deferring enabled.
You will know it worked when: WooCommerce > Status > Scheduled Actions shows woocommerce_send_queued_transactional_email running on time, and Check & Log Email records actual sends for every new test order.
Payment gateway-related email failures
Order emails depend on order statuses. Order statuses depend on the payment gateway. If the gateway is misconfigured, orders stall in Pending and no customer email fires. This is the same failure class as the "Pending payment" section above, but it is worth calling out the gateway-side checks explicitly:
- Stripe: open Developers > Webhooks in the Stripe dashboard. The endpoint pointing at
/?wc-api=wc_stripemust show recent successful deliveries. If it shows 401 or 403, the site is blocking Stripe's IPs or has basic auth enabled. - PayPal Standard: IPN must be enabled at the PayPal account level, and your IPN URL under Profile > Instant payment notifications must point at
https://yoursite.nl/?wc-api=WC_Gateway_Paypal. PayPal logs IPN delivery attempts under the IPN History view. - Mollie, Adyen, Buckaroo, MultiSafepay, Sisow: each has its own webhook URL in the gateway dashboard, usually of the form
/?wc-api=<gateway_name>. Check the gateway's delivery log for 4xx or 5xx responses. - Manual bank transfer: orders legitimately stay in "On hold" until you manually mark them as processed. The "Customer On Hold Order" email fires on the transition to On hold, not the customer Processing email. If you want customers to get a receipt sooner, enable that specific email type.
You will know it worked when: the gateway dashboard shows successful webhook delivery for recent orders, the orders move to Processing without manual intervention, and the emails fire automatically.
SMTP configuration for WooCommerce
There is no WooCommerce-specific SMTP configuration. WooCommerce inherits whatever transport is configured at the WordPress level. The moment you install and configure an SMTP plugin in WordPress, every WooCommerce email routes through it without further setup.
For the complete walkthrough of installing a plugin, picking a sending provider (Gmail, SendGrid, Mailgun, Amazon SES, Postmark) and verifying delivery end-to-end, see WordPress SMTP configuration. Once that walkthrough is complete, a WooCommerce test order should produce a logged send in the SMTP plugin's log with the same From address you configured there.
One WooCommerce-specific note: if you run a high-volume store and send marketing mail as well as transactional mail, it is worth splitting the two across different sending streams. Use a transactional provider (Postmark, Amazon SES) for order emails and a separate stream or subdomain (mail.yoursite.nl) for marketing. That way a spam complaint on a newsletter does not damage the deliverability reputation of your order receipts. The SPF, DKIM and DMARC for WordPress email deliverability article covers the alignment requirements for running two streams.
Email logging to diagnose delivery problems
A logging plugin is the difference between "WooCommerce didn't send it" (which you can prove or disprove) and "nobody knows what happened". Install one on every WooCommerce store, even when everything currently works.
What to look for when a customer reports "I never got my order email":
- A log row for the expected email exists, status "Sent". WooCommerce triggered the email and
wp_mail()accepted it. The problem is between the sending host and the recipient's inbox. Check the message headers for the From address, open the recipient's spam folder, and if possible check the recipient's own bounce logs. A "sent" status in a WordPress log never proves delivery, only that PHPMailer accepted the handoff. WordPress Trac #23642 documents thatwp_mail()can returntrueeven when sending failed at the transport layer. - The log row exists with status "Failed". The log plugin caught a PHPMailer exception. Click the row to see the error message. Common causes: expired API key on the SMTP provider, an invalid From address, or the From address was changed to something the provider has not verified.
- No log row exists at all.
wp_mail()was never called for this order's email. WooCommerce did not trigger it. Go back to the "order status" section and the "HPOS" section.
On revenue-critical stores I also hook wp_mail_failed into a must-use plugin so failures land in wp-content/debug.log as well as in the plugin log. That catches exceptions that happen before the log plugin sees the message. Create a file called log-mail-errors.php in wp-content/mu-plugins/ using your hosting panel's file manager or SFTP client, with this content:
<?php
// wp-content/mu-plugins/log-mail-errors.php
add_action( 'wp_mail_failed', function ( WP_Error $error ) {
error_log( 'wp_mail_failed: ' . $error->get_error_message() );
error_log( print_r( $error->get_error_data(), true ) );
} );
You can then check debug.log through the error log viewer in your hosting panel, or by opening wp-content/debug.log in the file manager.
You will know it worked when: every resend from WooCommerce > Orders produces a row in the log plugin within a second or two, with status "Sent" and the correct From address.
When to escalate
If you have worked through order status, settings, HPOS, the background queue and SMTP and customer order emails still do not arrive reliably, gather the following before opening a ticket with your host or developer:
- WooCommerce version and WordPress version (Tools > Site Health > Info).
- Whether HPOS is enabled, and whether Settings > Advanced > Features lists any incompatible plugins.
- The status of one affected order, and whether the gateway dashboard shows successful webhook delivery for it.
- A screenshot of WooCommerce > Settings > Emails showing the relevant email as enabled, with the sender options at the bottom.
- The exact error string from
wp-content/debug.log(the line starting withwp_mail_failed:), if one appears. - The log row for the affected email from Check & Log Email or your SMTP plugin's log, including the From address and the recipient.
- A copy of the
woocommerce_send_queued_transactional_emailScheduled Actions log for the past 24 hours. - The full list of active plugins (Tools > Site Health > Info > Active Plugins).
- Your current SPF, DKIM and DMARC records as published in DNS.
A support engineer with all of that can usually resolve the problem in a single session. Without it, expect several rounds of back-and-forth.
How to prevent it from coming back
- Always route WooCommerce mail through an authenticated SMTP provider from day one, not PHP
mail(). - Keep HPOS compatibility visible: subscribe to the changelogs of every email-adjacent plugin on the store so you see the compatibility releases when they land.
- Run Action Scheduler on a real server cron, not WP-Cron over HTTP. The in-HTTP cron is the most common silent failure for deferred transactional email.
- Keep a mail logger installed with 30 to 90 days retention. The first time a customer complains, you want a log to read, not a guess to make.
- Monitor the payment gateway's webhook delivery dashboard weekly. Failed webhooks are the leading cause of orders stuck in "Pending" that never trigger emails.
- Never point the WooCommerce "From" address at a mailbox that nobody monitors. A hard bounce on your own From address breaks deliverability in ways that take weeks to recover from.