Coming Soon - The Watchman Tower WordPress plugin is currently in development. This documentation is prepared in advance for the upcoming release.
What is a Heartbeat?
A heartbeat is a regular health check signal sent from your WordPress site to Watchman Tower’s monitoring servers.
Purpose: Proves your site is alive and provides health metrics for analysis.
Analogy: Like a patient’s heartbeat monitor in a hospital - continuous signals indicate life; absence indicates a problem.
Heartbeat Architecture
High-Level Flow
Components
JavaScript Loop Client-side timer that triggers heartbeats at configured intervals
WP Admin AJAX WordPress AJAX handler that receives JS requests
PHP Collector Backend code that collects site metrics
API Client HTTP client that sends data to Watchman Tower
JavaScript Heartbeat Loop
How It Works
The plugin injects JavaScript code into WordPress admin pages that:
Starts on page load - Initializes when dashboard is accessed
Runs in background - Continues even if you navigate away (within admin)
Self-scheduling - Each heartbeat schedules the next one
Respects paused state - Stops when monitoring is paused
Code Overview
// Simplified version of the actual code
( function ( $ ) {
"use strict" ;
// Plugin data from PHP (localized script)
const wthbData = {
ajaxUrl: "/wp-admin/admin-ajax.php" ,
nonce: "abc123xyz" ,
interval: 300 , // seconds
paused: false ,
};
let heartbeatTimer = null ;
// Initialize heartbeat loop
function init () {
if ( wthbData . paused ) {
console . log ( "Heartbeat monitoring is paused" );
return ;
}
scheduleNextHeartbeat ();
}
// Schedule the next heartbeat
function scheduleNextHeartbeat () {
clearTimeout ( heartbeatTimer );
heartbeatTimer = setTimeout (() => {
sendHeartbeat ();
}, wthbData . interval * 1000 );
}
// Send heartbeat via AJAX
function sendHeartbeat () {
$ . ajax ({
url: wthbData . ajaxUrl ,
method: "POST" ,
data: {
action: "wthb_send_heartbeat" ,
nonce: wthbData . nonce ,
},
success : function ( response ) {
console . log ( "Heartbeat sent successfully" );
updateDashboard ( response . data );
scheduleNextHeartbeat (); // Schedule next
},
error : function ( xhr , status , error ) {
console . error ( "Heartbeat failed:" , error );
scheduleNextHeartbeat (); // Retry anyway
},
});
}
// Manual heartbeat trigger
$ ( "#wthb-send-now" ). on ( "click" , function ( e ) {
e . preventDefault ();
sendHeartbeat ();
});
// Start when DOM ready
$ ( document ). ready ( init );
})( jQuery );
Script Enqueue
// Plugin enqueues script on admin pages
function wthb_enqueue_scripts () {
wp_enqueue_script (
'wthb-heartbeat' ,
WTHB_PLUGIN_URL . 'assets/js/heartbeat.js' ,
[ 'jquery' ],
WTHB_VERSION ,
true
);
// Pass PHP data to JavaScript
wp_localize_script ( 'wthb-heartbeat' , 'wthbData' , [
'ajaxUrl' => admin_url ( 'admin-ajax.php' ),
'nonce' => wp_create_nonce ( 'wthb_heartbeat' ),
'interval' => get_option ( 'wthb_heartbeat_interval' , 300 ),
'paused' => get_option ( 'wthb_monitoring_paused' , false ),
'siteId' => get_option ( 'wthb_site_id' ),
'instanceId' => get_option ( 'wthb_instance_id' )
]);
}
add_action ( 'admin_enqueue_scripts' , 'wthb_enqueue_scripts' );
Timing Accuracy
Q: Is the interval exact?
A: No - there’s natural variation due to:
JavaScript timer precision (±100ms typical)
AJAX request/response time (500ms - 2s)
WordPress cron drift (if used)
Browser throttling (inactive tabs)
Example: 300s interval might actually be 298-305s in practice.
This variation is normal and doesn’t affect monitoring quality. Watchman Tower
accounts for timing jitter when calculating uptime.
PHP Heartbeat Handler
AJAX Action Registration
// Register AJAX handler for logged-in admins
add_action ( 'wp_ajax_wthb_send_heartbeat' , 'wthb_handle_heartbeat' );
function wthb_handle_heartbeat () {
// Verify nonce for security
check_ajax_referer ( 'wthb_heartbeat' , 'nonce' );
// Check user permissions
if ( ! current_user_can ( 'manage_options' )) {
wp_send_json_error ( 'Insufficient permissions' );
return ;
}
// Check if monitoring is paused
if ( get_option ( 'wthb_monitoring_paused' )) {
wp_send_json_error ( 'Monitoring is paused' );
return ;
}
// Rate limiting check
$last_heartbeat = get_transient ( 'wthb_last_heartbeat_time' );
if ( $last_heartbeat && ( time () - $last_heartbeat < 60 )) {
wp_send_json_error ( 'Rate limit exceeded. Wait 1 minute.' );
return ;
}
// Collect metrics and send
$result = wthb_send_heartbeat_to_api ();
if ( $result [ 'success' ]) {
set_transient ( 'wthb_last_heartbeat_time' , time (), 60 );
wp_send_json_success ( $result [ 'data' ]);
} else {
wp_send_json_error ( $result [ 'message' ]);
}
}
Security Measures
What: WordPress nonce (number used once) for CSRF protection How:
Generated server-side, passed to JavaScript, verified on AJAX request php check_ajax_referer('wthb_heartbeat', 'nonce'); Prevents: Cross-site
request forgery attacks
What: Ensures only admins can trigger heartbeatsif ( ! current_user_can ( 'manage_options' )) {
wp_send_json_error ( 'Insufficient permissions' );
}
Prevents: Non-admin users triggering API calls
What: Prevents excessive heartbeat sendingLimit: 1 per minute (for manual heartbeats)$last_time = get_transient ( 'wthb_last_heartbeat_time' );
if ( $last_time && ( time () - $last_time < 60 )) {
wp_send_json_error ( 'Rate limit exceeded' );
}
Prevents: API abuse and server overload
Heartbeat Data Collection
Metrics Gathering
When a heartbeat is triggered, the plugin collects:
$data [ 'wordpress' ] = [
'version' => get_bloginfo ( 'version' ),
'multisite' => is_multisite (),
'locale' => get_locale (),
'timezone' => wp_timezone_string (),
'debug_mode' => WP_DEBUG ,
'memory_limit' => WP_MEMORY_LIMIT
];
$data [ 'php' ] = [
'version' => phpversion (),
'memory_limit' => ini_get ( 'memory_limit' ),
'max_execution_time' => ini_get ( 'max_execution_time' ),
'upload_max_filesize' => ini_get ( 'upload_max_filesize' ),
'post_max_size' => ini_get ( 'post_max_size' )
];
$data [ 'server' ] = [
'software' => $_SERVER [ 'SERVER_SOFTWARE' ],
'php_sapi' => php_sapi_name (),
'mysql_version' => $wpdb -> db_version (),
'disk_free' => disk_free_space ( '/' ),
'disk_total' => disk_total_space ( '/' ),
'memory_usage' => memory_get_usage ( true ),
'memory_peak' => memory_get_peak_usage ( true )
];
$data [ 'health' ] = [
'ssl_enabled' => is_ssl (),
'permalink_structure' => get_option ( 'permalink_structure' ),
'cron_disabled' => defined ( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ,
'file_editing_disabled' => defined ( 'DISALLOW_FILE_EDIT' ) && DISALLOW_FILE_EDIT
];
$active_plugins = get_option ( 'active_plugins' );
$data [ 'plugins' ] = [];
foreach ( $active_plugins as $plugin ) {
$plugin_data = get_plugin_data ( WP_PLUGIN_DIR . '/' . $plugin );
$data [ 'plugins' ][] = [
'name' => $plugin_data [ 'Name' ],
'version' => $plugin_data [ 'Version' ],
'author' => $plugin_data [ 'Author' ]
];
}
$theme = wp_get_theme ();
$data [ 'theme' ] = [
'name' => $theme -> get ( 'Name' ),
'version' => $theme -> get ( 'Version' ),
'author' => $theme -> get ( 'Author' ),
'parent' => $theme -> parent () ? $theme -> parent () -> get ( 'Name' ) : null
];
Complete Payload Example
{
"instance_id" : "WTHB-A7B3C9" ,
"site_id" : "550e8400-e29b-41d4-a716-446655440000" ,
"timestamp" : 1705329600 ,
"wordpress" : {
"version" : "6.4.2" ,
"multisite" : false ,
"locale" : "en_US" ,
"timezone" : "America/New_York" ,
"debug_mode" : false ,
"memory_limit" : "256M"
},
"php" : {
"version" : "8.2.0" ,
"memory_limit" : "512M" ,
"max_execution_time" : "30" ,
"upload_max_filesize" : "64M" ,
"post_max_size" : "64M"
},
"server" : {
"software" : "Apache/2.4.58" ,
"php_sapi" : "fpm-fcgi" ,
"mysql_version" : "8.0.35" ,
"disk_free" : 42949672960 ,
"disk_total" : 107374182400 ,
"memory_usage" : 52428800 ,
"memory_peak" : 62914560
},
"health" : {
"ssl_enabled" : true ,
"permalink_structure" : "/%postname%/" ,
"cron_disabled" : false ,
"file_editing_disabled" : true
},
"plugins" : [
{
"name" : "Yoast SEO" ,
"version" : "21.7" ,
"author" : "Team Yoast"
},
{
"name" : "WooCommerce" ,
"version" : "8.4.0" ,
"author" : "Automattic"
}
],
"theme" : {
"name" : "Astra" ,
"version" : "4.5.2" ,
"author" : "Brainstorm Force" ,
"parent" : null
}
}
See complete metrics reference →
API Communication
Sending to Watchman Tower
function wthb_send_heartbeat_to_api () {
// Collect data
$payload = wthb_collect_metrics ();
// Add authentication
$headers = [
'Content-Type' => 'application/json' ,
'x-api-key' => get_option ( 'wthb_api_key' ),
'User-Agent' => 'WatchmanTower-WordPress/' . WTHB_VERSION
];
// Send to API
$response = wp_remote_post ( 'https://api.watchmantower.com/public/v1/heartbeat' , [
'headers' => $headers ,
'body' => wp_json_encode ( $payload ),
'timeout' => 30 ,
'sslverify' => true
]);
// Handle response
if ( is_wp_error ( $response )) {
return [
'success' => false ,
'message' => $response -> get_error_message ()
];
}
$code = wp_remote_retrieve_response_code ( $response );
$body = wp_remote_retrieve_body ( $response );
if ( $code === 200 ) {
// Update last successful heartbeat
update_option ( 'wthb_last_heartbeat' , time ());
return [
'success' => true ,
'data' => json_decode ( $body , true )
];
} else {
return [
'success' => false ,
'message' => "API returned status code: $code "
];
}
}
API Endpoint
URL: https://api.watchmantower.com/public/v1/heartbeat
Method: POST
Authentication: x-api-key header with site-specific key
Timeout: 30 seconds
Response:
{
"success" : true ,
"message" : "Heartbeat received" ,
"site_status" : "up" ,
"next_expected" : 1705330200
}
Error Handling
Error: Request takes > 30 seconds Causes: - Slow server response -
Network congestion - DNS issues Handling: - Logged to WordPress
debug.log - Displayed in dashboard - Automatic retry on next cycle
Error: 401 Unauthorized Causes: - Invalid API key - Expired token -
Site disconnected in dashboard Handling: - User prompted to reconnect -
Dashboard shows “Connection Lost” status - Heartbeat loop continues (for when
fixed)
Error: 429 Too Many Requests Causes: - Too many heartbeats in short
time - Shared IP hitting global limits Handling: - Respects Retry-After
header - Automatically backs off - Dashboard shows warning
Error: 500, 502, 503, 504 Causes: - Watchman Tower API issues -
Temporary outage Handling: - Exponential backoff retry - User notified
in dashboard - Automatic recovery when API restored
WordPress Cron Integration
Scheduled Events
While the heartbeat is primarily driven by JavaScript, a WordPress cron event serves as a backup:
// Register cron event on plugin activation
function wthb_activate () {
if ( ! wp_next_scheduled ( 'wthb_heartbeat_event' )) {
$interval = get_option ( 'wthb_heartbeat_interval' , 300 );
wp_schedule_event ( time (), 'wthb_custom_interval' , 'wthb_heartbeat_event' );
}
}
register_activation_hook ( __FILE__ , 'wthb_activate' );
// Add custom cron interval
add_filter ( 'cron_schedules' , 'wthb_cron_interval' );
function wthb_cron_interval ( $schedules ) {
$interval = get_option ( 'wthb_heartbeat_interval' , 300 );
$schedules [ 'wthb_custom_interval' ] = [
'interval' => $interval ,
'display' => sprintf ( __ ( 'Every %d seconds' ), $interval )
];
return $schedules ;
}
// Cron handler
add_action ( 'wthb_heartbeat_event' , 'wthb_cron_heartbeat' );
function wthb_cron_heartbeat () {
// Only send if monitoring not paused
if ( ! get_option ( 'wthb_monitoring_paused' )) {
wthb_send_heartbeat_to_api ();
}
}
Why Two Methods?
Method Primary Use Advantage Disadvantage JavaScript Loop Active monitoring Runs when admin is active Requires browser open WP-Cron Backup / Low-traffic sites Doesn’t need browser Subject to cron timing issues
Best of both worlds: JavaScript for reliability when admins are active, WP-Cron as fallback for quiet periods.
On low-traffic sites, consider disabling WP-Cron and using system cron for
more reliable heartbeats.
Monitoring Dashboard Updates
Real-Time UI Updates
After each successful heartbeat:
function updateDashboard ( data ) {
// Update status badge
$ ( "#wthb-status" ). removeClass (). addClass ( data . status );
// Update last heartbeat time
$ ( "#wthb-last-beat" ). text ( data . last_heartbeat_relative );
// Update uptime percentage
$ ( "#wthb-uptime" ). text ( data . uptime_percentage + "%" );
// Update downtime percentage
$ ( "#wthb-downtime" ). text ( data . downtime_percentage + "%" );
// Show success message
showNotification ( "Heartbeat sent successfully" , "success" );
}
The status widget auto-refreshes every 60 seconds to show latest data from Watchman Tower:
setInterval ( function () {
$ . get (
wthbData . ajaxUrl ,
{
action: "wthb_get_site_status" ,
nonce: wthbData . statusNonce ,
},
function ( response ) {
updateDashboard ( response . data );
}
);
}, 60000 ); // 60 seconds
Troubleshooting Heartbeat Issues
Symptoms: Last heartbeat time not updatingCheck:
Is monitoring paused?
JavaScript errors in browser console?
WordPress cron working? (wp cron event list)
Firewall blocking outbound HTTPS?
Debug: # Check if cron event exists
wp cron event list --search=wthb
# Test manual API call
wp eval 'wthb_send_heartbeat_to_api();'
Symptoms: Heartbeats sent but returning errors Check: 1. API key still
valid? 2. Server can reach api.watchmantower.com? 3. PHP timeout limits
adequate? 4. Memory limits sufficient? Debug: php // Enable debug logging define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); // Check debug.log for errors tail -f wp-content/debug.log | grep wthb
Heartbeat interval not changing
Explanation: New interval applies to NEXT heartbeat cycle. Solution:
Send a manual heartbeat to apply immediately.
JavaScript loop not starting
Check: 1. Script enqueued? (View page source, search for heartbeat.js)
2. jQuery loaded? 3. wthbData object available? (console.log(wthbData))
Fix: Clear cache and hard reload (Ctrl+Shift+R)
Resource Usage
Per heartbeat:
Memory: ~2-5 MB during collection
CPU: ~50-100ms processing time
Bandwidth: ~2-5 KB upload
API call: 1 HTTPS POST request
At 300s interval (default):
Daily: 288 heartbeats
Monthly: ~8,640 heartbeats
Bandwidth/month: ~21-43 MB
Verdict: Negligible performance impact on modern hosting. Even at 60s
intervals, resource usage is minimal.
Optimization Tips
Increase Interval For low-priority sites, use 600-3600s
Pause During Peak Pause during known high-traffic events
Use System Cron More reliable than WP-Cron on low-traffic sites
Monitor Logs Watch for errors indicating issues
Next Steps