Skip to main content
Robert Michalski

Perform actions asynchronously in WordPress

Are critical user actions such as checkout, register or login taking a long time to complete or even failing? #

To get a good user experience and high conversion rate, it's necessary that critical or important user actions complete as fast as possible with minimum risk of failing.

Examples of critical or important user actions

How can we make critical or important actions faster? #

One way is to do less work before responding to the user. Certain tasks can be scheduled to run in another request as soon as possible after the critical request has been completed.

Examples of tasks to offload

All these tasks can be offloaded and executed as part of other requests.

For instance during checkout, sending the order confirmation email and adding the customer to an emailing list can be scheduled instead of executed right away as part of payment processing.

Why is scheduling an action faster than executing it right away? #

Scheduling a task in WordPress involves saving data to a database, it only creates a couple of rows in some tables. Since the database software is usually running locally on the same server or another server within the same data-center (connected via multi-gigabit low-latency networks), writing to the database is very fast and unlikely to fail.

Network requests often go over the public Internet to 3rd-party services, running servers that might be located half-way across the globe. The signals making up the network requests must first travel over great distances, thousands of miles, across countries and even oceans to distant servers. Oftentimes there are several servers communicating behind the scenes, before a response is sent back across the globe again. It takes time for the signals to travel, even with the speed of light, the other servers need to perform work and communicate with yet more servers and services. At any point along the way there can be failures, network congestion, server errors in the services, retries etc.

This all adds up real fast, causing latencies and in worst case failed requests.

How to schedule an action in WordPress #

There are two easy ways to schedule actions in WordPress, wp-cron or action-scheduler.

The most comprehensive and reliable way is using action scheduler, however it might not always be available. WP-Cron is part of WordPress core and therefore is always available.

Scheduling an action using either method requires that there is an action hook registered with a handler function.

add_action('my_async_hook', 'my_async_handler', 10, 3); // registers handler function as WordPress hook, remember to set the correct number of arguments
//add_action('my_async_handler', 'my_async_handler', 10, 3); // both the hook and function can have the same name
function my_async_handler($arg1, $arg2, $arg3) {
    ... // perform the action, send email, perform network requests etc.
}

Schedule action using WP-Cron #

WP-Cron is always available and runs either as part of visitor requests or using a dedicated CRON job.

// schedule using WP-Cron
if (!wp_next_scheduled('my_async_hook', [123, 'test', ['name' => 'Robert']])) {
    wp_schedule_single_event(time() + 10, 'my_async_hook', [123, 'test', ['name' => 'Robert']]);
}

As reusable function

function YOUR_PREFIX_schedule_async_cron($hook, $args = []) {
    // no need to clean wordpress single cron events, they are not retained after completion
    return
        wp_next_scheduled($hook, $args) ||
        wp_schedule_single_event(time() + 10, $hook, $args);
}

// schedule using WP-Cron like a Boss
$result = YOUR_PREFIX_schedule_async_cron('my_async_hook', [123, 'test', ['name' => 'Robert']]));

Schedule action using action-scheduler #

Before using action-scheduler make sure it's available, otherwise the critical user action might fail with a "white screen of death" or output of PHP error. Action-scheduler can be installed as a stand-alone plugin, added as a dependency in a custom plugin or theme, or use another plugin (for instance WooCommerce) which include it.

if (function_exists('as_enqueue_async_action')) {
    $scheduled = as_enqueue_async_action(
    	'my_async_hook',
    	[
    		123,
    		'test',
    		['name' => 'Robert']
			],
			'my-custom-actions'
		);
}

As a reusable function with optional cleanup

function YOUR_PREFIX_schedule_async_action($hook, $args = [], $group = '', $remove_completed = false) {
    if (!function_exists('as_enqueue_async_action')) {
        return false
    }
    // Schedule action using action scheduler when available
    if ($remove_completed) {
        $ids = as_get_scheduled_actions(
            [
                'hook' => $hook,
                'args' => $args,
                'status' => ActionScheduler::store()::STATUS_COMPLETE
            ],
            'ids'
        );
        foreach ( $ids as $id ) {
            ActionScheduler::store()->delete_action($id);
        }
    }
    return as_enqueue_async_action($hook, $args, $group);
}

// schedule using Action Scheduler like a boss
$scheduled = YOUR_PREFIX_schedule_async_action('my_async_hook', [123, 'test', ['name' => 'Robert']]), 'my-custom-actions');

Schedule action using action-scheduler and use WP-Cron as backup #

function YOUR_PREFIX_schedule_async_action($hook, $args = [], $group = '', $remove_completed = false) {
    // Schedule action using wp-cron when action scheduler is not available
    if (!function_exists('as_enqueue_async_action')) {
        return YOUR_PREFIX_schedule_async_cron($hook, $args);
    }
    // Optional cleanup of completed actions
    if ($remove_completed) {
        $ids = as_get_scheduled_actions(
            [
                'hook' => $hook,
                'args' => $args,
                'status' => ActionScheduler::store()::STATUS_COMPLETE
            ],
            'ids'
        );
        foreach ( $ids as $id ) {
            ActionScheduler::store()->delete_action($id);
        }
    }
    return as_enqueue_async_action($hook, $args, $group);
}

// Schedule async actions like a real Boss with fallbacks and clean-ups
$scheduled = YOUR_PREFIX_schedule_async_action('my_async_hook', [123, 'test', ['name' => 'Robert']]), 'my-custom-actions', true);

Example: Scheduling sending email as scheduled action #

// called by action scheduler or wp-cron
add_action('YOUR_PREFIX_send_email_async', 'YOUR_PREFIX_send_email', 10, 3); // 4 params in case headers are needed

function YOUR_PREFIX_send_email_async($email, $subject, $message, $headers = '') {
    // optional filters
    /*
    $email = apply_filters('YOUR_PREFIX_send_email_async_recipient', $email);
    $subject = apply_filters('YOUR_PREFIX_send_email_async_subject', $subject);
    $message = apply_filters('YOUR_PREFIX_send_email_async_message', $message);
    $headers = apply_filters('YOUR_PREFIX_send_email_async_headers', $headers);
    */
    if (!is_email($email)) {
        return false;
    }
    if (is_array($message)) {
        $message = wp_json_encode($message, JSON_PRETTY_PRINT);
    }
    return YOUR_PREFIX_schedule_async_action(
        'YOUR_PREFIX_send_email_async',
        [
            'email' => $email,
            'subject' => $subject,
            'message' => $message,
            'headers' => $headers
        ],
        'YOUR_PREFIX_send_email_async',
        //true // clean completed action or skip to keep a log of completed actions
    );
}
function YOUR_PREFIX_send_email($email, $subject, $message, $headers = '') {
    // optional filters
    /*
    $email = apply_filters('YOUR_PREFIX_send_email_recipient', $email);
    $subject = apply_filters('YOUR_PREFIX_send_email_subject', $subject);
    $message = apply_filters('YOUR_PREFIX_send_email_message', $message);
    $headers = apply_filters('YOUR_PREFIX_send_email_headers', $headers);
    */
    if (!is_email($email)) {
        return false;
    }
    if (is_array($message)) {
        $message = wp_json_encode($message, JSON_PRETTY_PRINT);
    }
    echo '<br>Sending email to ' . $email . '<br><br>'; // will be logged and visible in wp-backend
    return wp_mail($email, $subject, $message, $headers);
}

// schedule sending an email ASAP, like a Boss
$scheduled = YOUR_PREFIX_send_email_async('[email protected]', 'Thank you for your order!', 'Thank you for your order...Order nbr:...');