Slow Down or Disable the WordPress Heartbeat API

The WordPress Heartbeat API keeps the browser and the server in touch: every 15 to 60 seconds an open admin tab sends a request to admin-ajax.php. It powers post locking, the “session expired” login popup and live notices from plugins. On shared hosting, a few editors with many open tabs add up to a lot of PHP requests, and some hosts throttle or suspend accounts for it.

This snippet modifies the Heartbeat API without a plugin: it slows it down everywhere, switches it off on the front end, and can also switch it off in wp-admin while keeping it on the editor screens where it matters.

The snippet

<?php
/**
 * Slow down the WordPress Heartbeat API and switch it off where it is not needed.
 */

// ===== Config =====
function dpc_heartbeat_config(): array {
	$config = array(
		// Seconds between Heartbeat requests, 15-120. WordPress default: 60.
		'interval'      => 120,
		// Stop Heartbeat on the front end (only if no other script there depends on it).
		'disable_front' => true,
		// Stop Heartbeat in wp-admin, except on the post editor screens.
		// This also turns off the "Your session has expired" login popup on those screens.
		'disable_admin' => false,
	);
	// ===== End of config =====

	return (array) apply_filters( 'dpc_heartbeat_config', $config );
}

add_filter( 'heartbeat_settings', 'dpc_heartbeat_interval' );

function dpc_heartbeat_interval( $settings ) {
	$config = dpc_heartbeat_config();

	if ( ! empty( $config['interval'] ) ) {
		$settings['interval'] = max( 15, min( 120, (int) $config['interval'] ) );
	}

	return $settings;
}

add_action( 'wp_enqueue_scripts', 'dpc_heartbeat_front', PHP_INT_MAX );

function dpc_heartbeat_front(): void {
	$config = dpc_heartbeat_config();

	if ( ! empty( $config['disable_front'] ) ) {
		// Dequeue, not deregister: scripts that depend on Heartbeat still get it.
		wp_dequeue_script( 'heartbeat' );
	}
}

// wp-admin loads Heartbeat through the session-expired check (wp-auth-check) on every screen.
add_filter( 'wp_auth_check_load', 'dpc_heartbeat_admin', 10, 2 );

function dpc_heartbeat_admin( $show, $screen = null ) {
	$config = dpc_heartbeat_config();

	if ( empty( $config['disable_admin'] ) || ! $screen instanceof WP_Screen ) {
		return $show;
	}

	// Keep it on the editor: post locks and the block editor need it.
	return 'post' === $screen->base ? $show : false;
}

add_action( 'admin_enqueue_scripts', 'dpc_heartbeat_admin_dequeue', PHP_INT_MAX );

function dpc_heartbeat_admin_dequeue(): void {
	$config = dpc_heartbeat_config();
	$screen = get_current_screen();

	if ( ! empty( $config['disable_admin'] ) && $screen && 'post' !== $screen->base ) {
		wp_dequeue_script( 'heartbeat' );
	}
}

How it works

  • heartbeat_settings sets the options WordPress prints for heartbeat.js. The snippet sets interval to your value, limited to 15–120 seconds. Post locks expire after 150 seconds, so a longer interval would let two people edit the same post.
  • On the front end the snippet dequeues heartbeat at the very end of wp_enqueue_scripts. It does not deregister it: a plugin script that lists Heartbeat as a dependency still loads it and keeps working.
  • In wp-admin, Heartbeat is loaded on every screen by the session-expired check, wp-auth-check. With disable_admin on, the wp_auth_check_load filter turns that check off on every screen except the post editor, and Heartbeat is dequeued there too.
  • The older version of this post deregistered heartbeat on init everywhere. WordPress still registers wp-auth-check and autosave with Heartbeat as a dependency, so every admin page logged a “called incorrectly” notice for missing dependencies, and post locks stopped working in the editor.

Add it with Scripts Organizer

In Scripts Organizer go to Code Blocks → Add New and give the block a name. Set Trigger location to Everywhere, tick PHP under Script location, paste the code into the PHP editor, edit the config and publish. Leave Action/Hook empty.

The settings are needed on the front end and in wp-admin, so Everywhere is the right trigger; Conditions would only cover the front end.

The same code also works unchanged in a small plugin or an mu-plugin.

Customise it

  • Only slow it down: set 'disable_front' => false and keep disable_admin off. Then only the interval changes.
  • Fewest requests: set 'interval' => 120 and 'disable_admin' => true. The editor screens keep Heartbeat at two minutes.
  • Override the config from another snippet with the dpc_heartbeat_config filter, for example to switch it off in wp-admin only for some roles.

Gotchas

  • With disable_admin on, users whose login expires on a screen without Heartbeat find out only when they save. The editor screens still show the login popup.
  • WooCommerce, page builders, membership and chat plugins sometimes use Heartbeat for live updates. If something stops refreshing, set the interval back to 60 first.
  • The browser itself already slows Heartbeat down to 120 seconds in background tabs, so a lower interval mostly matters for the tab people are looking at.
  • To see what still loads, open the Network tab and filter on admin-ajax.php, or list everything hooked to heartbeat_received.

In the Scripts Organizer library Import it with one click from Scripts Organizer.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You’ll also get DPlugins product updates. No spam, and you can unsubscribe anytime.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You'll also get DPlugins product updates. No spam, and you can unsubscribe anytime.