
Defer WooCommerce emails for a specified time after the normal delivery time.
Update 7 March 2022: I changed the scheduling code to use Action Scheduler that is included with WooCommerce. If WP Cron is disabled this change will help as WooCommerce will run Action Scheduler automatically. To view the scheduled action you do not need to install a plugin, instead go to WooCommerce/Status/Scheduled Actions.
Update 4 May 2022: I have significantly changed the algorithm for the execution of the deferred emails. Instead of calling the email function I call action hook they were added to. This makes the code generic i.e. it will run all functions/emails attached to the deferred action hook.
Update 27 October 2023: Solei asked about delaying WooCommerce Advanced Notifications emails. After investigating I decided to write the code as a separate plugin: Defer WooCommerce Advanced Notifications emails.
After completing the code for Add Tracking Info to WooCommerce order Elizabeth reported intermittent issues where the tracking info was not in the Order Completed emails. I suspected that there was a small delay with Shippo pushing the tracking data back to her site. Shippo suggested delaying the Order Completed email to allow for the small delay (which could be a few minutes).
Stepping through the code
When WooCommerce 3.0 was released it deferred transactional emails (i.e. emails sent when the order status changes) by 5 seconds (increased to 10 seconds in later release). Deferring the emails was primarily to speed up the checkout process. In version 3.0.3 this was disabled by default but could be enabled with a filter.
I read through the WooCommerce code and later stepped through it with Visual Studio Code. The debugging confirmed what my code reading found – there was no WooCommerce filter to change the delay time from 10 seconds. I even looked at filters in the wp_schedule_event() function but there didn’t appear to be enough data to idenfity WooCommerce email events.
Eureka moment
I found that another developer, Dan Wich, encountered the same issue. While typing a comment on his post I got an idea – set up my own scheduled event!
It happens frequently when, after extensive investigation and debugging of an issue, writing a post on a forum gives me an idea for a solution. It’s a sort of ‘a problem shared is a problem halved‘ type thing.
I had used WP Cron about 3 years ago but I needed to reread the wp_schedule_single_event() docs and look for examples. In a later version of my code I changed it to use as_schedule_single_action(), the Action Scheduler equivalent of wp_schedule_single_event().
My experiments in trying to find a filter were very helpful in finding the existing WooCommerce filters that I needed to use. The final logic is quite simple:
- Enable deferred emails
- Examine an email just before it is to be sent – disallow it and schedule a future event to send it (include the order ID and email identifier)
- The scheduled event function directly triggers the email for the specified order ID.
When an email is deferred you can see it in a list of scheduled actions (under WooCommerce/Status/Scheduled Actions). I installed Advanced Cron Manager plugin while testing. The scheduled event is called ‘send_deferred_woocommerce_email‘. The arguments are the order number and the email identifier.

Future Proofing
My initial code only deferred the Order Completed email but I generalised the code to allow any WooCommerce transactional email to be deferred and a different delay time for each one.
Thanks for this it really saved my life trying to defer on-hold emails and every search I did was to speed up mails not actually slow them down. I have added a comment on your gist with the on-hold options. Thanks Again.
Hi,
Ive been trying your script but I cannot get it to work properly. As suggested I created a Plugin from the script. Activated it.
Now when I use the option within an order to send an email to customer, like the invoice its shows up directly in my inbox, no delay…
I want to delay the email that is sent when a user finalizes his payment and goes to thank you page.
‘woocommerce_order_status_processing’ => array( ‘WC_Email_Customer_Processing_Order’, $this->default_defer_time ),
What am I doing wrong here? Please help.
Im using WooCommerce 4.3.2.
Thanks!
I emailed AD and he needed to uncomment the ‘woocommerce_order_status_pending_to_processing’ item.
I confirmed that was the required one by adding some code to the plugin code:
add_action('woocommerce_order_status_changed', 'dcwd_order_status_changed', 10, 4); function dcwd_order_status_changed( $id, $from, $to, $order ) { error_log( 'Order ID: ' . $id ); error_log( 'Status from: ' . $from ); error_log( 'Status to: ' . $to ); }
And got the following in wp-content/debug.log:
[15-Sep-2020 09:11:56 UTC] Order ID: 7959 [15-Sep-2020 09:11:56 UTC] Status from: pending [15-Sep-2020 09:11:56 UTC] Status to: processing
@Donovan – Thanks for the additional code. I have updated the gist to include these. I’m delighted that you found it useful – it took me a long while to figure it all out.
FYI, the link back to the blog post from Github is missing the s in emails.
@Todd – Thank you. Fixed now. I think I changed the url while writing the post and forgot to update the Github link.
Hi Damien, Where do we add this code? To which file in child theme? Thanks
Man you my friend are a genius, I just used the code on my website and the response time has jumped 1000%. Thank you so much
@Hassan – it sounds like you found out where to add the code. You can add it to a theme’s functions.php (or, even better, a child theme’s functions.php). Even better is to read my post about How to use my code snippets and put the code in its own plugin file.
This is awesome! Thank you so much for the help! The script made it possible for me to collect data from external sources, giving it enough time before sending the processing email.
This script is super good for its purpose and works as it should.
Thanks!
@Adrian – Thanks. I added the code where were discovered the correct statuses to use as a reply to your @AD comment. I have also updated the GIST code with this function.
Great stuff Damien! I’m hitting a snag and I’m not sure if it’s me or something that needs to be tweaked.
I’ve 2 emails that get triggered on woocommerce_order_status_pending_to_on-hold
1.) the WC_Email_New_Order email which goes to the admin 2.) the WC_Email_Customer_On_Hold_Order email which goes to the customer
I only want to defer the WC_Email_Customer_On_Hold_Order email. I tried just deferring the customer email as follows:
$this->email_id_to_class = array( 'woocommerce_order_status_pending_to_on-hold' => array( 'WC_Email_Customer_On_Hold_Order', $this->default_defer_time ), // Order on hold. );
Which works fine for the customer email. But the admin email never gets sent. I’m guessing it’s because there’s more than 1 email attached to the woocommerce_order_status_pending_to_on-hold hook?
Equally, I tried just adding the following:
$this->email_id_to_class = array( 'woocommerce_order_status_pending_to_on-hold' => array( 'WC_Email_New_Order', $this->default_defer_time ), // New order 'woocommerce_order_status_pending_to_on-hold' => array( 'WC_Email_Customer_On_Hold_Order', $this->default_defer_time ), // Order on hold. );
Which doesn’t seem to work.Any ideas? Colm
I have the same problem, and I have modified the number of seconds for $this->default_defer_time = 600; to 120, but it doesn’t work, it’s the same as the letter will be received after 10 minutes, and the New Order order of admin email is also not I will receive it, can you help me deal with it? thank you very much! !
@Scott – Does the error_log file show you the status that you expect? See the order_status_changed() function. What does it write to the error_log file?
Please email me the $this->email_id_to_class array. I would like to see what emails are set to be delayed.
Any reply or solution? please share thank you !
Colm and I exchanged a few emails. The root issue is that my code does not account for when one email trigger sends more than 1 email. For the moment Colm has hard coded the emails being sent. Changing this would require a considerable rewrite of the code but I don’t have the time at the moment.
@Colm: I have updated the code to be generic, independent on how many functions are attached to an action. I would be grateful if you could try it out.
Hey Damien, I installed your plugin on my site and can’t seem to get it working. I’m using it in conjunction with WC Vendors trying to hold the vendor notifications to give the site time to process bigger orders. Currently, I’ve changed line 35 to say:
‘woocommerce_order_status_completed’ => array( ‘WCVendors_Vendor_Notify_Order’, $this->default_defer_time ),
But when I try to use the plugin, even just straight out of the box for WC_Email_Customer_Completed_Order, it seems like it blocks ALL woocommerce notification emails from sending.
Any ideas on what might be going on?
For what its worth I’ve also tried just using add_filter( ‘woocommerce_defer_transactional_emails’, ‘__return_true’ ); and that just stops everything from sending all together, maybe I don’t quite understand what the base function is supposed to do?
Any help is much appreciated. Thanks
Hello, We do have to delay the WooCommerce order processing email for 1 minute. But I do not get that wo work with that code. Do I have to change and add lines? :)
I sat down this morning to begin solving this problem. I’m glad I searched before I started writing code!
Thank you for sharing your hard work with the world, and thank you for writing such clean, readable, and well-commented code!
@Reed – Thank you. If you find any issues (and solve them), please let me know.
I am no programmer but can tell you know what you are doing. Any chance you can provide me with the appropriate code to defer only COMPLETED ORDERS for 5 days?
@Doron – the code in the post defers emails for COMPLETED ORDERS for 10 minutes (600 = 10 x 60).
For 5 days you would change line 30 to:
$this->default_defer_time = 432000;
This is fantastic and exactly what I needed. While I was implementing it, I think (but am not sure) that it had captured a bunch of deferred email and then sent it out.
Are you aware of a situation in which this might happen? I would like to make sure it does not.
@Darcy: I have not seen that happen and I don’t know how it could.
I’m using your plugin, Add Tracking to WooCommerce Email and it works great (thanks), but like the scenario you describe above, the info wasn’t being included in the email unless the order was saved first. So I implemented this plugin to fix that but now the email isn’t sent at all.
I reduced the time to 120s but no dice. I deactivated the plugin and the email is sent normally.
Help. What am I missing?
@David: I know about the issue you describe. WooCommerce sends the email before CMB2 saves the tracking data. I did a lot of deep debugging and found the hook involved. I have updated the code so that CMB2 saves the data before the emails are sent.
Many many thanks for sharing your code! This has helped immensely. Using this with the Code Snippets plugin I get an error on line 30:
"$this->default_defer_time = was 600;"
When I remove the “was” and just leave the “600”, code snippets stops displaying a warning, however the WooCommerce logs show: CRITICAL syntax error, unexpected ‘600’ (T_LNUMBER) in /home/examplecom/public_html/wp-content/plugins/code-snippets/php/admin-menus/class-edit-menu.php(248) : eval()’d code on line 30. What am I missing here? Thanks in advance if you do have time to help me with this.@Gary – The ‘was’ was a typo and I’ve updated the code to remove it. When I was testing I had
50 /*was 600*/
and I removed the ’50’ and the comment marks but not the ‘was’.I know you’ve already removed ‘was’ and it’s causing the error. If it continues to happen please email me the code snippet so I can test it on my local environment. My email address is on my contact page.
Hi Damien, Brilliant code here! Well done. Is there a way to delete the deferred email in the action scheduler automatically if the status changes? Case would be if I delay an on hold email for 48 hours and if the order is paid within this period (status changes to processing) the delayed email on hold email will not be sent. Christian
Hi Damien, after installing WP Cron Manager, I do not see the scheduled event ‘send_deferred_woocommerce_email‘. Do I need to manually add it in? If so, what do I put for the arguments? Would it be $id and woocommerce_order_status_completed?
Thanks!
@Ken – Ah, I forgot to update the text and screenshot in my post to reflect the changes since I changed the code to use Action Scheduler instead of WP Cron. You no longer need a plugin to see the scheduled task. It will be in WooCommerce/Status/Scheduled Actions.
I have updated the post and the screenshot. Thank you for your comment.
Thank you Damien! I see it now :)
Not working for me. Used the exact code in my child theme’s Functions.php file, and modified the defer time to 3600, and the following for my custom order status:
$this->email_id_to_defer = array( 'woocommerce_order_status_return-no-issues' => $this->default_defer_time, )
As soon as an order hits that status, the email goes out immediately.
Here’s the exact code with modifications:
https://codefile.io/f/TaMZfXcYZK4X3euf3Dvw
@Matt: The Custom Order Statuses for WooCommerce plugin does not use the WooCommerce status change hooks so my code doesn’t see these emails being triggered.
That plugin uses a custom hook (
'woocommerce_order_status_changed_to_woocos'
) and a custom email class (based on WooCommerce’s WC_Email). To defer the emails for these custom statuses would require special code to handle this particular plugin. It’s not particularly easy because the plugin developers create the class instances without assigning them to a variable or providing a get_instance() member function to access the instance. I would need to access the instance to disable sending the email initially and then to get the plugin to send the email at a later time. It would be an easy thing for the developers to add a get_instance() function to the classes.My code that sends the email would also have to be able to distinguish a custom status from a default status and handle it differently. It might need the developers of the status plugin to add some extra functions to verify that a custom status is valid (so that I would use their API and not copy their code which might change in the future) and a way to send an email.
Hello.. Thank you for this great plugin, works 100% :)
I have also added delay for woocommerce_new_order and also this works great. But tested on woocommerce_low_stock and woocommerce_no_stock and those gave me a fatal error.
Any idea on this? I think it would be great to delay also these as they can really add up on bigger orders, and then slow down checkout process.
Brgds Rune
@Rune: The other emails are for orders so maybe the stock emails are different as they are about products. I will investigate.
Hello Damien,
Thank you for sharing your code to defer emails. Works great except for custom order status. I am using NP Quote Request Woocommerce and have added status id to defer but it’s not catching it.
have tried: ‘woocommerce_order_status_gplsquote-req’ ‘woocommerce_order_status_pending_to_gplsquote-req’
Any thoughts?
@Jay – From looking at the code it does not check whether emails are deferred (with the WooCommerce feature). My code enables that feature and processes the action that it runs. If the plugin developer could add this ability then my code would work.
The plugin class that sends the emails does not extent WC_Emails so does not inherit the
queue_transactional_email
function that my code hooks into. The developer could add this feature.Hey Damien! Great to hear your code is working fine! Is there any chance that I just delay the processing email? Unfortunately, the buyer is receiving this email first and then the on-hold email, which should be the other way around. Thanks!
@Leo: Yes, you can just delay the processing email. On line 40 it has:
//'woocommerce_order_status_pending_to_processing' => $this->default_defer_time,
Uncomment this line.
You can comment out lines 35 and 36 so that the note and Completed emails are not delayed.
'woocommerce_new_customer_note' => $this->default_defer_time, 'woocommerce_order_status_completed' => $this->default_defer_time,
Wowo, thank you for your fast response!
So, unfortunately didn´t work. But I notice that the payment gateway first puts the order in pending, then on-hold and then in processing. In the line you suggested, it appears to be pending to processing. Would that be a problem?
Hi Damien,
I’m using the Advanced Notifications plugin from WooCommerce and need to delay the sendouts. Is this possible with your code? I can’t get it to work.
This plugin: https://woocommerce.com/products/advanced-notifications/
Thank you for great resources!
// Solei
@Solei: I looked at the plugin. As it works differently from WooCommerce emails I decided to write a new plugin to defer its emails: https://www.damiencarbery.com/2023/10/defer-woocommerce-advanced-notifications-emails-for-a-few-minutes/.