Cron and Async Jobs
This page covers BuddyNext’s scheduled jobs and background-work model: the recurring and single-shot cron hooks in Free and Pro, the Action Scheduler fan-out pattern used for heavy per-member work, and the cron policy every host must respect. Read it before adding a new scheduled job or wiring a new write fan-out.

Overview / Contract
Section titled “Overview / Contract”BuddyNext is built to be fast on a vanilla install with zero server changes. Three rules govern every scheduled job:
- Pick the lightest mechanism that fits. Derivable data is computed lazily and cached - no job. Event responses use a single-shot async action. Only genuinely always-on work (digests, retries, reconciles) uses a recurring schedule, at the lowest acceptable cadence.
- Action Scheduler first, WP-Cron fallback. When Action Scheduler is present, recurring and async actions run through it under the
buddynextgroup so they are observable and retryable in Tools > Scheduled Actions. When it is absent, the same hooks run on WP-Cron. - Never force-disable WP-Cron. The plugin never defines
DISABLE_WP_CRONand never requires a system cron to be fast. See Cron policy below.
The full engineering standard lives in the Background-Jobs standard (docs/standards/BACKGROUND-JOBS.md); this page documents the jobs that exist today.
Scheduled jobs (Free)
Section titled “Scheduled jobs (Free)”Free registers a set of recurring maintenance jobs plus a handful of reactive single-event hooks. Handlers are plain add_action callbacks - Action Scheduler and WP-Cron both fire the same hook name.
Recurring jobs
Section titled “Recurring jobs”Registered on wp_loaded by Core\CronScheduler under the Action Scheduler group buddynext (WP-Cron fallback when AS is absent), except where a different registrar is noted.
| Hook | Schedule | What it does |
|---|---|---|
buddynext_daily_digest |
Daily | Sends the daily activity digest. |
buddynext_weekly_digest |
Weekly | Sends the weekly activity digest. |
buddynext_cleanup_tokens |
Daily | Prunes expired auth / verification tokens. |
buddynext_cleanup_notifications |
Weekly | Prunes read notification rows older than 90 days. |
buddynext_cleanup_activity_log |
Weekly | Prunes activity-log rows past the data-retention window. |
buddynext_cleanup_email_log |
Weekly | Prunes bn_email_log rows past the data-retention window. |
buddynext_recount_stats |
Daily | Reconcile pass for counters. Counters are maintained incrementally on every write; this only repairs drift. |
buddynext_daily_queue_check |
Daily | Sweeps the moderation queue for items needing a daily reconcile (aging reports, expiring strikes). Registered by the moderation listener. |
buddynext_publish_scheduled_sweep |
Hourly | Safety sweep that publishes any scheduled post whose time has passed. Registered by Feed\ScheduledPostsPublisher. |
Single-event / reactive hooks
Section titled “Single-event / reactive hooks”Armed on demand and self-clearing - they do not poll when there is no work.
| Hook | When it fires | What it does |
|---|---|---|
buddynext_publish_scheduled |
Armed at the next due scheduled post’s time | Publishes due scheduled posts, then re-arms for the next one (or stays disarmed). Free-owned - runs with Pro absent. |
bn_onboarding_nudge_24h |
Single event, 24h after signup | Sends the first onboarding nudge to a member who has not finished the setup steps. Armed per user at registration; self-clears once onboarding completes. |
bn_onboarding_nudge_72h |
Single event, 72h after signup | Sends the second onboarding nudge if the member is still incomplete at 72 hours. Same per-user arming model as the 24h nudge. |
buddynext_reindex_all_cron |
Single event | Runs a full rebuild of the bn_search_index table (members, posts, spaces, hashtags). Dispatched on demand - for example after a settings change that invalidates the index - not on a fixed cadence. |
buddynext_webhook_deliver |
Single event, per outbound delivery | Delivers one outbound webhook off-request. Enqueued async (Action Scheduler) when a webhook fires. |
buddynext_webhook_retry_single |
Single event, per failed delivery | Retries a failed webhook delivery with exponential backoff (300s base, up to 3 attempts). |
edd_sl_sdk_weekly_license_check_{slug} |
Weekly | License-validation check from the bundled EDD Software Licensing SDK. Gates plugin updates only, never functionality. |
Note: There is no recurring webhook-retry poll. Outbound delivery uses single events (
buddynext_webhook_deliver/buddynext_webhook_retry_single) scheduled per delivery with exponential backoff, so nothing polls when the queue is empty. The old custom sub-hour recurrences (buddynext_1min,buddynext_5min,buddynext_30min) were removed in the cron-minimisation pass - every remaining recurring job uses a built-indaily/weekly/hourlyrecurrence.
Scheduled jobs (Pro)
Section titled “Scheduled jobs (Pro)”Pro adds its own recurring jobs. Unlike Free they do not share the buddynext group - each Pro subsystem uses its own Action Scheduler group, so it can be observed and cancelled independently in Tools > Scheduled Actions.
| Hook | Schedule | Group | What it does |
|---|---|---|---|
buddynextpro_broadcast_send_pending |
Every 5 minutes | buddynextpro_email |
Sends the next batch of a pending email broadcast. Handled by Email\BroadcastService; self-(un)scheduling - armed when a broadcast is queued, disarmed when the send completes. |
buddynextpro_drip_tick |
Hourly | buddynextpro_email |
Advances drip email sequences - enqueues the next due email for each enrolled member. Handled by Email\DripEnrollmentService / Email\DripService; self-(un)scheduling - stays disarmed when no enrollments are active. |
buddynextpro_expire_subscriptions |
Daily | buddynextpro |
Expires membership subscriptions past their end date and revokes the matching entitlements. Handled by Membership\SubscriptionService. |
buddynextpro_ai_mod_sweep |
Configurable cadence (from the AI-moderation settings) | buddynextpro_ai_moderation |
Recurring AI-moderation sweep over recent content. Handled by Moderation\AiModerationSweep. |
buddynextpro_ai_mod_cleanup |
Daily | buddynextpro_ai_moderation |
Prunes old AI-moderation bookkeeping rows. Handled by Moderation\AiModerationSweep. |
Note: Pro does not register its own scheduled-post publish cron. Publishing is Free-owned -
Feed\ScheduledPostsServicedelegates the actual publish to Free’sFeed\ScheduledPostsPublisher(thebuddynext_publish_scheduledsingle event above).
The Action Scheduler fan-out pattern
Section titled “The Action Scheduler fan-out pattern”Some events touch many rows. When a member posts in a space with 100,000 members, naively inserting 100,000 notification rows inside the request would time the request out. BuddyNext caps the synchronous cost and pages the rest through Action Scheduler.
The pattern, as implemented for new-space-post notifications:
- The triggering request processes a first bounded batch synchronously (a keyset query limited to
SPACE_FANOUT_BATCHmembers). Small spaces finish here with no background job at all. - If a full batch came back, more members remain. The request enqueues an async action, resuming from the last processed
user_id(keyset, neverOFFSET):
if ( function_exists( 'as_enqueue_async_action' ) ) { as_enqueue_async_action( 'buddynext_async_space_post_fanout', array( 'post_id' => $post_id, 'space_id' => $space_id, 'author_id' => $author_id, 'after_user_id' => $after_user_id, // keyset cursor ), 'buddynext' // group );}- The worker processes one bounded batch, then re-enqueues itself for the next page until the roster is exhausted. Neither the request nor any single scheduled action ever loads or loops the whole roster.
- When Action Scheduler is absent (local/test), the worker drains the remaining batches inline, still bounded to one batch per query, so every member is still notified.
Hashtag indexing follows the same shape on a smaller scale. When a post is created, the hashtag listener dispatches buddynext_async_index_hashtags instead of extracting and syncing tags inside the request:
if ( function_exists( 'as_enqueue_async_action' ) ) { as_enqueue_async_action( 'buddynext_async_index_hashtags', $args, 'buddynext' );} else { wp_schedule_single_event( time(), 'buddynext_async_index_hashtags', $args );}Async hooks you can extend
Section titled “Async hooks you can extend”| Hook | Fired when | Args |
|---|---|---|
buddynext_async_space_post_fanout |
A space post needs background notification fan-out (RECORD stage) beyond the first batch | array{ post_id, space_id, author_id, after_user_id } |
buddynext_async_space_post_emails |
DELIVER stage: send the space new-post emails for a recipient batch, off the fan-out task (self-paginating in chunks of 50) | array{ post_id, space_id, author_id, recipients } |
buddynext_async_index_hashtags |
A post or other content needs hashtag extraction + sync | object_type, object_id, content |
buddynext_reindex_all |
A full search-index rebuild is requested | none |
Rules for any new fan-out you add:
- One group. Always pass
'buddynext'as the group so the action is observable and bulk-cancelable in Tools > Scheduled Actions. - Idempotent handlers. Action Scheduler retries on failure; the handler must tolerate re-running the same batch.
- Keyset, not OFFSET. Page with a
last_idcursor so large rosters never get slower as you page deeper. - Cap the synchronous slice. Do the first bounded batch in-request, only background the remainder.
Cron policy - never force, detect and guide
Section titled “Cron policy - never force, detect and guide”BuddyNext never changes a site-wide cron setting on the owner’s behalf.
- The plugin never defines
DISABLE_WP_CRON. That constant disables WP-Cron for every plugin on the site (backups, WooCommerce, email queues) and silently breaks them if no real system cron is wired. - Action Scheduler runs fine off WP-Cron by default. Running it off a real system cron is an optional, owner-applied server optimization - never a requirement for BuddyNext to be fast.
- The plugin detects the failure case and guides the admin. The Tools health check reports whether WP-Cron is disabled and whether any scheduled actions are overdue. Only when the site is genuinely stalled (WP-Cron off and actions overdue) does it surface a warning with a ready-to-paste system-cron line built from the site’s own URL:
*/5 * * * * wget -q -O - 'https://SITE/wp-cron.php?doing_wp_cron' >/dev/null 2>&1Notes / gotchas
Section titled “Notes / gotchas”- Schedule on
wp_loadedorinit, neverplugins_loaded. Action Scheduler is not initialized untilinit; anas_schedule_*call before that silently no-ops. If you cleared the WP-Cron event first, the job ends up unscheduled entirely. Always verify scheduling happened after the fact. - Deactivation clears both systems. A job’s deactivation handler must call
as_unschedule_all_actions( $hook, array(), 'buddynext' )andwp_clear_scheduled_hook( $hook )so nothing is orphaned. - Free / Pro boundary. Pro registers its jobs independently on its own boot, using the same AS-first / WP-Cron-fallback pattern. Unlike Free’s shared
buddynextgroup, each Pro subsystem uses its own group (buddynextpro_email,buddynextpro,buddynextpro_ai_moderation); scheduled-post publishing is not a Pro job - Pro delegates it to Free’sScheduledPostsPublisher. - Pruning. Action Scheduler’s retention clears the
actionscheduler_*tables on its own schedule (on by default). Leave it enabled so the logs do not bloat.
See also the Background-Jobs standard for the full decision tree and copy-paste patterns, and the Scale Contract for the fan-out and indexing rules that shaped these jobs.

