July 23, 2022

Optimizing Performance for Repeated Hook Execution in WordPress

In WordPress and WooCommerce development, it’s common to use action hooks like save_post or woocommerce_update_order to trigger custom functionality. However, these hooks can fire multiple times during a single operation—leading to repeated execution of time-consuming tasks such as email sending or external API calls.

This article discusses why this happens, and introduces two optimization strategies:

  • Temporarily removing the hook from within the hooked function
  • Offloading tasks to scheduled actions (asynchronous execution)

Let’s explore both approaches in detail.


The Problem: Repeated Execution of Hooks

When using WordPress hooks like save_post, developers often notice the callback functions being triggered multiple times—especially if you’re still using old-style meta boxes instead of the modern Gutenberg editor. This can be problematic if the hooked functions are performing costly operations.

A similar situation occurs in WooCommerce with the woocommerce_update_order hook, which can fire multiple times during order creation or updates.

Let’s examine an example:

add_action( 'woocommerce_update_order', function( $order_id ) {
    $email = ''; // Set your test email address
    wp_mail( $email, 'Test Email', sprintf( 'Order %d has been updated.', $order_id ) );
});

After adding this code, you might be flooded with test emails each time an order is updated—because woocommerce_update_order runs multiple times per request.

Since sending emails is a relatively slow operation, especially when SMTP connections are involved, this can significantly slow down the checkout or order update process.


Solution 1: Remove the Hook Within Itself

The simplest way to prevent multiple executions is to remove the hook from within the function that’s attached to it:

add_action( 'woocommerce_update_order', 'wprs_order_update', 25, 2 );

function wprs_order_update( $order_id, $order ) {
    remove_action( 'woocommerce_update_order', __FUNCTION__, 25, 2 );

    // Your custom processing logic here
}

This ensures the function only runs once per request, even if the hook is triggered multiple times.

⚠️ Caveat

This method may miss later executions of the hook, which might contain important updates by WooCommerce or other plugins. For example:

1. woocommerce_update_order → our function runs and removes itself
2. woocommerce_update_order → skipped
3. woocommerce_update_order → skipped
4. woocommerce_update_order → WooCommerce logic executes, but we no longer listen

If this side effect is acceptable, this is a simple and effective solution. Otherwise, consider the second approach.


Solution 2: Use Scheduled Actions (Async Processing)

A more robust solution is to defer your processing to a background task using a scheduled action. WooCommerce and the Action Scheduler library make this easy.

Here’s how you can do it:

add_action( 'woocommerce_update_order', 'wprs_maybe_order_update' );

function wprs_maybe_order_update( $order_id ) {
    as_schedule_single_action( 
        time() + 5, // Delay execution by 5 seconds
        'wprs_update_order', // Custom hook name
        array( $order_id ),  // Arguments to pass
        '',                  // Group name (optional)
        true                 // Unique per order ID
    );
}

add_action( 'wprs_update_order', function( $order_id ) {
    $order = wc_get_order( $order_id );
    // Place your custom logic here (e.g., send email, update data, etc.)
});

💡 Key Notes:

  • We use as_schedule_single_action() to schedule the task once, even if the hook is triggered multiple times.
  • Setting the $unique parameter to true ensures only one job is scheduled per order at a time.
  • You could also use as_enqueue_async_action() for immediate, non-delayed async execution.

This approach offloads resource-intensive operations from the main process and avoids repeat execution altogether.


Conclusion

If you’re using hooks like save_post or woocommerce_update_order to perform heavy operations, you must account for the fact that these hooks may run more than once per request.

To optimize performance and prevent issues:

  • Use remove_action() if your logic is self-contained and doesn’t rely on subsequent hook triggers.
  • Use as_schedule_single_action() for delayed, single-time async execution that won’t block frontend performance or interfere with WooCommerce internals.

These strategies ensure your site remains responsive and reliable—even under heavy load or with complex workflows.