Heartbeat API

Heartbeat API is a lightweight way to periodically (every 15-120 seconds) poll the server for new data and then use it on the client side (browser).

Heartbeat API was introduced in WordPress 3.6 and was initially needed for two things:

  • To notify the user that a post is being edited by someone else at the moment.
  • To check if the authorization session has expired and to launch a popup window asking to re-authenticate.

Over time, this mechanism has been utilized by themes and plugins.

By default, Heartbeat API works only in the admin panel, but it can easily be used on the front end as well.

Heartbeat API is based on classic AJAX in WordPress.

How it works

  1. After the page loads, a special JavaScript code (heartbeat code) starts a timer. At regular intervals, the JS event heartbeat-send is triggered, and the JS functions hooked to this event are executed.

  2. The JS functions formulate and send data to the admin-ajax.php file. In this file, the WP hook is triggered:

    • wp_ajax_heartbeat - if authorized
    • wp_ajax_nopriv_heartbeat - if not authorized

    By default, both of these hooks are hooked to functions with the same name: wp_ajax_heartbeat() and wp_ajax_nopriv_heartbeat(). Thus, during a heartbeat request in PHP, one of the functions is triggered:

  3. The function then calls three hooks through which you essentially need to work with Heartbeat:

    If authorized:

    // if there is $_POST['data']
    $response = apply_filters( 'heartbeat_received', $response, $data, $screen_id );
    
    // always triggered
    $response = apply_filters( 'heartbeat_send', $response, $screen_id );
    
    // always triggered
    do_action( 'heartbeat_tick', $response, $screen_id );

    If not authorized:

    // if there is $_POST['data']
    $response = apply_filters( 'heartbeat_nopriv_received', $response, $data, $screen_id );
    
    // always triggered
    $response = apply_filters( 'heartbeat_nopriv_send', $response, $screen_id );
    
    // always triggered
    do_action( 'heartbeat_nopriv_tick', $response, $screen_id );

    $screen_id will equal front for the frontend.

  4. After all the above hooks are executed, PHP returns the data (variable $response) to the user (in the browser) in JSON format.

  5. Upon receiving data in JS, the heartbeat-tick event is triggered.

    Based on the received data, various tasks are performed: prompting to re-authenticate, messages from the client to the manager, messages from the Telegram chat, notifications about new comments, and so on.

Using Heartbeat API

Heartbeat API is by default available only in the admin area. To use this mechanism on the front end, you need to enqueue the script heartbeat.js or specify it as a dependency in your script.

// Option 1. Enqueue the JS script heartbeat
wp_enqueue_script( 'heartbeat' );

// Option 2. Enqueue heartbeat as a dependency in our script
wp_enqueue_script( 'script-name', get_template_directory_uri() .'/js/example.js', array('heartbeat'), '1.0', true );

Next, to use the Heartbeat API, you need to go through three stages. It doesn't matter whether this is done for the front end or the admin area.

1. Sending data to the server

The JS event heartbeat-send is triggered before sending data to the server, and this is the best moment to add your data to the Heartbeat data collection.

In the default WordPress build, you can see something like this in the browser tab during the ajax request:

interval: 60
_nonce: bab7ce80c5
action: heartbeat
screen_id: dashboard
has_focus: false

Let's add our data to this collection:

According to good practice, JS code should be placed in a file and enqueued on the admin_enqueue_scripts hook (if it concerns the admin area), but for simplicity, we will output it on the admin_print_footer_scripts hook.

<?php
// Enqueue our script in the admin area
add_action( 'admin_print_footer_scripts', function () {
	?>
	<script>
	jQuery(document).on( 'heartbeat-send', function (event, data) {
		// Add our data to the Heartbeat data collection.
		data.myplugin_field = 'some_data';
	});
	</script>
	<?php
} );

In the browser tab, you can see how the data has been sent:

data[myplugin_field]: some_data
interval: 60
_nonce: bab7ce80c5
action: heartbeat
screen_id: dashboard
has_focus: false

In the example, a simple string some_data was sent, but you can send any data: numbers, arrays, objects, etc.

2. Receiving data on the server and responding

First, let's see what the server returns by default:

{
	"wp-auth-check":true,
	"server_time":1520878390
}

On the heartbeat_received hook, you can augment the array that is formed by the core/plugins/theme with your data:

// Filter for working with received Heartbeat data.
add_filter( 'heartbeat_received', 'myplugin_receive_heartbeat', 10, 2 );

/**
 * Receives Heartbeat data and forms a response.
 *
 * @param array $response Heartbeat data to send to the frontend.
 * @param array $data     Data sent from the frontend (unslashed).
 *
 * @return array
 */
function myplugin_receive_heartbeat( $response, $data ) {
	// If our data did not come, return the original response.
	if ( empty( $data['myplugin_field'] ) ) {
		return $response;
	}

	/**
	 * Access our data.
	 * The array key matches the property of the object in JS where we placed the data.
	 */
	$received_data = $data['myplugin_field'];

	// For example, let's count how many characters are in the provided string.
	$count_symbol = mb_strlen( $received_data );

	$response['myplugin_strlen'] = 'The number of characters in the provided phrase is ' . $count_symbol;

	// Return the augmented Heartbeat array with our data.
	return $response;
}

Now the server will return the following set of data:

{
	"myplugin_strlen":"The number of characters in the provided phrase is 9"
	"wp-auth-check":true,
	"server_time":1520878390
}

3. Processing the server response

After the server returns the object with data, let's process it:

<?php
add_action( 'admin_print_footer_scripts', function () {
	?>
	<script>
		jQuery( document ).on( 'heartbeat-tick', function ( event, data, textStatus, jqXHR ) {
			// event - event object
			// data - incoming data
			// textStatus - status of the request execution, for example success.
			// jqXHR - request object
			console.log(event, data, textStatus, jqXHR);

			// Check if our data is present and if not - stop the script.
			if ( ! data.myplugin_strlen ) {
				return;
			}

			// Display the data on the screen via alert()
			alert( data.myplugin_strlen );
		});
	</script>
	<?php
} );

We have covered three stages, but not every task requires the first or third stage.

Under the hood

PHP

The PHP functionality is concentrated in the ajax-actions.php file.

Function wp_ajax_heartbeat()

This function should not be used; it is automatically called in the admin-ajax.php file through ajax hooks. It checks the request and triggers the hooks related to Heartbeat: heartbeat_send, heartbeat_tick.

In other words, the function processes Heartbeat requests from registered users. Before processing the request, it checks for the presence of a nonce code. If it is missing, it interrupts the operation and returns an error. If the nonce code has expired, it returns information about this, and a popup appears on the page asking to re-authenticate. It also adds the server time in the response using the time() function.

You can add your functionality via filters and actions. See below.

Filter heartbeat_received

Filters the received Heartbeat data. It triggers if there is data in $data.

$response(array)
Data for the Heartbeat response.
$data(array)
Data sent in the $_POST array.
$screen_id(string)
Screen ID. In PHP, it matches $current_screen->id and the global pagenow in JS.

Example

add_filter( 'heartbeat_received', 'myplugin_receive_heartbeat', 10, 3 );

function myplugin_receive_heartbeat( $response, $data, $screen_id ) {
	// If our data did not come, return the original response.
	if ( empty( $data['myplugin_field'] ) ) {
		return $response;
	}

	/**
	 * Access our data.
	 * The array key matches the handle in JS that was assigned to the data.
	 */
	$received_data = $data['myplugin_field'];

	// Let's assume a string is coming, and for example, let's count how many characters are in this string.
	$count_symbol = mb_strlen( $received_data );

	$response['myplugin_field_strlen'] = 'The number of characters in the provided phrase is ' . $count_symbol;

	// Return the augmented Heartbeat array with our data.
	return $response;
}

Filter heartbeat_send

Filters the Heartbeat response data.

$response(array)
Data for the Heartbeat response.
$screen_id(string)
Screen ID.

Example from WordPress core

In the Heartbeat response, information about the user's authorization status is added.

add_filter( 'heartbeat_send', 'wp_auth_check' );

function wp_auth_check( $response ) {
	$response['wp-auth-check'] = is_user_logged_in() && empty( $GLOBALS['login_grace_period'] );
	return $response;
}

Simple example:

Let's add information about the number of unmoderated comments to the Heartbeat response.

add_filter( 'heartbeat_send', 'myplugin_send_heartbeat', 10, 2 );

function myplugin_send_heartbeat( $response, $screen_id ) {
	$comments_count = wp_count_comments();
	$response['moderated'] = $comments_count->moderated;
	return $response;
}

Event heartbeat_tick

Similar to heartbeat_send, but this is an event, and nothing needs to be changed in it; it is customary to perform some actions before the server responds to the browser. That is, it is an excellent place for implementing any actions, such as sending an email.

It triggers after the Heartbeat data is formed but before the server sends it to the client.

$response(array)
Data for the Heartbeat response.
$screen_id(string)
Screen ID.

Example

If certain data is received, notify the administrator by email.

add_action( 'heartbeat_tick', 'myplugin_heartbeat_tick', 10, 2 );

function myplugin_heartbeat_tick( $response, $screen_id ) {
	if( ! empty($response['some_key']) ){
		wp_mail('[email protected]', 'Email Subject', 'Email Content');
	}
}

Filter heartbeat_settings

Allows you to change options in the JS object wp.heartbeat.settings.

// Changes the default server polling interval from 60 seconds to 30.
add_filter( 'heartbeat_settings', function ( $settings ) {
	$settings['mainInterval'] = 30;
	return $settings;
} );

Hooks and function wp_ajax_nopriv_heartbeat()

For interaction with unauthorized users on the front end, the wp_ajax_nopriv_heartbeat() function is triggered instead of the wp_ajax_heartbeat() function. It differs from the first only in that it does not check the nonce code and authorization status, and it contains hooks with different names but similar behavior:

JavaScript and jQuery

The JavaScript functionality is concentrated in the heartbeat.js file.

Event heartbeat-send

On this event, you can add your data to the Heartbeat collection before sending it to the server.

jQuery( document ).on( 'heartbeat-send', function ( event, data ) {
	// Add data to Heartbeat. Then on the server, data can be retrieved from the array by the key myplugin_customfield.
	data.myplugin_customfield = 'some_data';
});

Event heartbeat-tick

Triggered every time Heartbeat data is received from the server. On this event, we operate with the received data.

jQuery(document).on('heartbeat-tick', function (event, data, textStatus, jqXHR) {
	// event - event object
	// data - incoming data
	// textStatus - status of the request execution, e.g., success.
	// jqXHR - request object
	console.log(event, data, textStatus, jqXHR);

	// Check if data came from our plugin/theme.
	if (!data.data.myplugin_responce_data) {
		return;
	}

	alert('This data returned from my plugin/theme: ' + data.myplugin_responce_data);
});

You can also track the event for the required response by the data label:

$(document).on( 'heartbeat-tick.myplugin_responce_data', function( event, data, textStatus, jqXHR ) {
	// Code
});

Event heartbeat-error

Triggered every time a request to the server fails, i.e., when $.ajax().fail() is triggered. On this event, you can track what exactly went wrong and take action if necessary.

jQuery( document ).on( 'heartbeat-error', function ( jqXHR, textStatus, error ) {
	// jqXHR - request object.
	// textStatus - status of the request.
	// error - text version of the error (could be abort, timeout, error, parsererror, empty, unknown).
	console.log(jqXHR, textStatus, error);

	if( 'timeout' === error ){
		alert('30 seconds have passed, and the server has not responded. And that’s sad!');
	}
});

Other events

The Heartbeat API also includes several other events, but they are rarely used in development:

  • heartbeat-connection-lost
  • heartbeat-connection-restored
  • heartbeat-nonces-expired

JavaScript object wp.heartbeat

Working with events directly is not the only way to interact with the Heartbeat API. The heart of the mechanism is the class Heartbeat(), an instance of which is placed in the variable window.wp.heartbeat and is accessible to developers when writing JavaScript code.

Let's look at the methods of this class.

Method enqueue()

Adds data to the queue for sending in the next XHR. Since the data is sent asynchronously, this function does not return the XHR response. You can see the response on the heartbeat-tick event. If the same label is used multiple times, the data is not overwritten when the third argument noOverwrite is set to true. Use wp.heartbeat.isQueued('handle') to see if any data has already been queued for this label.

wp.heartbeat.enqueue( handle, data, noOverwrite );
handle(string)
Unique label (descriptor) of the sent data. On the backend, it is used as the array key to retrieve the transmitted values.
data(any)
Data being sent.
noOverwrite(boolean)
Whether to overwrite existing data in the queue.

Returns true if the data was queued and false if not.

// Add data to the queue for sending.
wp.heartbeat.enqueue(
	'my-plugin-data',
	{
		'param1': 'param value 1',
		'param2': 'param value 2',
	},
	false
);

Method getQueuedItem()

Returns the data queued by their label (Handle).

wp.heartbeat.getQueuedItem( handle );
handle(string)
Unique label (descriptor) of the sent data.
// Add data to the queue
wp.heartbeat.enqueue(
	'my-plugin',
	{
		'param1': 'value 1',
		'param2': 'value 2',
	},
	false
);

// Get the data
var myData = wp.heartbeat.getQueuedItem('my-plugin')

console.log(myData);
/*
will return
{
	'param1': 'value 1',
	'param2': 'value 2',
}
/

Method isQueued()

Checks if data with a specific label is in the queue.

wp.heartbeat.isQueued( handle );
handle(string)
Unique label (descriptor) of the sent data.

Returns true if the data is in the queue and false if not.

// In the console, it will display false
console.log( wp.heartbeat.isQueued('my-plugin') );

// Add data to the queue
wp.heartbeat.enqueue(
	'my-plugin',
	{
		'param1': 'value 1',
		'param2': 'value 2',
	},
	false
);

// In the console, it will display true
console.log( wp.heartbeat.isQueued('my-plugin') ); // true

Method dequeue()

Removes data from the queue by their label (handle).

wp.heartbeat.dequeue( handle );
handle (string)
Unique label (descriptor) of the sent data.
// Add data to the queue
wp.heartbeat.enqueue(
	'my-plugin',
	{
		'param1': 'value 1',
		'param2': 'value 2',
	},
	false
);

// In the console, it will display true, data 'my-plugin' is in the queue
console.log( wp.heartbeat.isQueued('my-plugin') ); // true

// Remove data from the queue
wp.heartbeat.dequeue('my-plugin');

// In the console, it will display false, data 'my-plugin' is not in the queue
console.log( wp.heartbeat.isQueued('my-plugin') ); // false

Method interval()

Sets or returns the server polling interval in seconds.

wp.heartbeat.interval( speed, ticks );
speed(string/number)
Polling speed in seconds. Can be 'fast' or 5, 15, 30, 60, 120, 'long-polling' (experimental). If the window is not in focus, the interval slows to 2 minutes. If the number of seconds is passed as a number, the type must be a number, not a string.
Default: 60
ticks(number)
This argument is applied if the speed argument is 'fast' or 5. It allows you to specify how many times to send requests at this interval. You can specify no more than 30, meaning that at most every 5 seconds the server will be polled for 2 minutes and 30 seconds. After that, the speed returns to the default value, which is 60 seconds.
Default: 30
// In the console, it will display 60 (default value)
console.log( wp.heartbeat.interval() ); // 60

wp.heartbeat.interval(30);

console.log( wp.heartbeat.interval() ); //> 30

Example of passing a variable as a number:

var interval = mytheme.custom_interval; // variable contains 30

wp.heartbeat.interval( parseInt( interval ) );

console.log( wp.heartbeat.interval() ); // 30

Method hasFocus()

Checks whether the window (or any local iframe within it) is focused or if the user is active. Returns true or false. It accesses the property settings.hasFocus. It works based on the native document.hasFocus() (if available) and updates settings.hasFocus every 10 seconds. If you need to check the focus more frequently than every 10 seconds, use document.hasFocus().

wp.heartbeat.hasFocus();

The code below will check every 5 seconds if the tab in the browser where the heartbeat is used is active and log the corresponding message to the browser console.

setInterval(function () {
	if (wp.heartbeat.hasFocus()) {
		console.log('Application is in focus.');
	} else {
		console.log('Focus lost. You minimized the browser, switched to another tab, or application.');
	}
}, 5000);

Method disableSuspend()

Disables suspension. This should only be used when heartbeat performs critical tasks such as auto-saving, post-locking, etc. Using this functionality on many screens can lead to overloading the user's hosting account if multiple browser windows/tabs remain open for an extended period.

wp.heartbeat.disableSuspend();

Method connectNow()

Immediately sends a heartbeat request regardless of the hasFocus state. It will not open two simultaneous connections. If a connection is in progress, it will connect again immediately after the current connection is completed. This is especially convenient for testing heartbeat functionality, as you can make a request immediately, including from the browser console.

wp.heartbeat.connectNow();

Method hasConnectionError()

Checks if there is a connection error.

if (wp.heartbeat.hasConnectionError()) {
	console.log('The last request was completed with an error.');
} else {
	console.log('No errors.');
}

Examples

There is nothing better than examples in the form of real plugins, which can always be found in the WordPress repository.

Comment Tracking in Admin

This plugin, for example, collects data about comments awaiting moderation during the heartbeat request and updates counters in the sidebar and admin bar. It works both in the admin panel and on the front end. And only for those users who have permission to moderate comments.

Download: Admin Comment Notice
The plugin is hosted on GitHub, so you can not only download it but also leave your wishes or report a bug.
Downloaded: 30, size:

Consists of two files.

admin-comment-notice.php

add_action( 'wp_enqueue_scripts', 'acn_enqueue_scripts' );
add_action( 'admin_enqueue_scripts', 'acn_enqueue_scripts' );
add_filter( 'heartbeat_send', 'acn_heartbeat_send' );

/**
 * Adds data to the heartbeat response.
 *
 * @param array $response
 *
 * @return array
 */
function acn_heartbeat_send( $response ) {
	if ( ! current_user_can( 'moderate_comments' ) ) {
		return $response;
	}

	$count = wp_count_comments();
	$count = absint( $count->moderated );
	$i18n  = number_format_i18n( $count );

	// Admin sidebar
	$menu = '<span class="awaiting-mod count-' . $count . '"><span class="pending-count">' . $i18n . '</span></span>';
	$menu = sprintf( __( 'Comments %s' ), $menu );

	// Admin bar
	$text = sprintf( _n( '%s comment awaiting moderation', '%s comments awaiting moderation', $count ), $i18n );
	$bar  = '<span class="ab-icon"></span>';
	$bar  .= '<span class="ab-label awaiting-mod pending-count count-' . $count . '" aria-hidden="true">' . $i18n . '</span>';
	$bar  .= '<span class="screen-reader-text">' . $text . '</span>';

	// Data
	$response['acn'] = array(
		'menu'  => $menu,
		'bar'   => $bar,
		'count' => $i18n,
	);

	return $response;
}

/**
 * Enqueues the plugin script.
 */
function acn_enqueue_scripts() {
	if ( is_admin_bar_showing() && current_user_can( 'moderate_comments' ) ) {
		$script_url = plugins_url( 'scripts.js', __FILE__ );
		wp_enqueue_script( 'acn-script', $script_url, array( 'heartbeat' ) );
	}
}

scripts.js

jQuery(document).on('heartbeat-tick', function (event, data) {

	// Check if our data is present and if not - stop the script.
	if (data.acn === undefined) {
		return;
	}

	// Find the containers where we will change the content.
	var $menu = jQuery('#menu-comments').find('.wp-menu-name');
	var $bar = jQuery('#wp-admin-bar-comments').find('a');

	// Change the content of the containers.
	jQuery($menu).html(data.acn.menu);
	jQuery($bar).html(data.acn.bar);
});

In WordPress by default

After sending a Heartbeat request, WP processes only a few operations by default. To find out what they are, let's look at what is attached to the hooks: heartbeat_received, heartbeat_send, and heartbeat_tick. That is, how WP alters the server response.

heartbeat_received

File wp-admin/includes/admin-filters.php:

add_filter( 'heartbeat_received', 'wp_check_locked_posts', 10,  3 );
add_filter( 'heartbeat_received', 'wp_refresh_post_lock',  10,  3 );
add_filter( 'heartbeat_received', 'heartbeat_autosave',    500, 2 );

File wp-includes/class-wp-customize-manager.php:

add_filter( 'heartbeat_received', array( $this, 'check_changeset_lock_with_heartbeat' ), 10, 3 );
heartbeat_send

File: wp-includes/default-filters.php

add_filter( 'heartbeat_send',        'wp_auth_check' );
add_filter( 'heartbeat_nopriv_send', 'wp_auth_check' );
heartbeat_tick

Nothing is attached by default.

Basic Heartbeat settings are established through the filter:

add_filter( 'heartbeat_settings', 'wp_heartbeat_settings' );

Literature

The materials used in the creation of the article include: