The scheduling check runs inside includes/bootstrap.php, which is loaded on every public page request. A last_schedule_check key in the options table stores the Unix timestamp of the last check. If fewer than 60 seconds have elapsed, the check is skipped entirely with no database query. If more time has passed, JekCMS queries the posts table for rows with status scheduled and scheduled_at <= NOW(), updates their status to published, and updates the timestamp.

The Trade-Off: Traffic-Dependent Accuracy

The main drawback is predictable: if no visitor arrives for two hours, posts scheduled within that window will not publish until the next request. For sites with consistent traffic — at least one visit per minute — this is invisible in practice. For overnight scheduled posts on low-traffic sites, delays of several hours are possible. This is a documented trade-off, not a bug.

cron_publish.php: The Reliable Alternative

v1.4.0 introduces a standalone publishing script at tools/cron_publish.php. Add it to your server's crontab: */5 * * * * php /path/to/jekcms/tools/cron_publish.php. When the script has run within the check window, the visitor-triggered check becomes a no-op — it detects the recent timestamp and skips. The two mechanisms coexist without conflict.

Measuring Your Real-World Delay Window

You can estimate your actual delay risk from server access logs. Count unique requests per hour over the past 30 days. If every hour shows at least one request, your maximum scheduling delay is 60 seconds.

If your quietest hours — typically 02:00 to 06:00 local time — show zero requests, those hours represent your exposure window. A site averaging 500 daily visits typically has zero-request gaps of 15 to 40 minutes overnight. Sites with 2,000+ daily visits almost never have a gap longer than 5 minutes.

How the Check Mechanism Works Internally

The internal flow follows a precise sequence designed to minimise database load:

// includes/bootstrap.php (simplified)
$lastCheck = (int) get_option('last_schedule_check');
$now = time();

if ($now - $lastCheck < 60) {
 return; // Skip - too recent
}

// Atomic timestamp update prevents race conditions
update_option('last_schedule_check', $now);

// Publish all due posts in a single query
$db->execute(
 "UPDATE posts SET status = 'published',
 published_at = NOW()
 WHERE status = 'scheduled'
 AND scheduled_at <= NOW()"
);

// Flush page cache for new content
Cache::flush('page');

The timestamp update happens before the publish query. This prevents a race condition where two simultaneous visitors could both pass the 60-second check and run the publish query twice. The second request would update zero rows, but the guard avoids the extra query entirely.

Cache Invalidation After Publishing

When scheduled posts are published, the page cache is flushed so visitors see the new content immediately. Without this step, a cached homepage might show stale content for up to 300 seconds (the default page cache TTL). The flush targets only page-level caches — object and query caches are left intact since they refresh on the next database read.

Setting Up the Cron Job

The setup varies by hosting environment. On a standard Linux server with SSH access:

# Open crontab editor
crontab -e

# Add this line (every 5 minutes)
*/5 * * * * php /var/www/html/jekcms/tools/cron_publish.php

# Verify the entry
crontab -l

The 5-minute interval is a practical default. For news sites that need near-real-time publishing, reduce it to every minute (* * * * *). For blogs with weekly schedules, every 15 minutes is sufficient. There is no performance penalty for shorter intervals — the script exits in under 10 milliseconds when no posts are due.

Monitoring Cron Execution

The cron script logs each run to logs/cron_publish.log with a timestamp and the count of published posts. A healthy log looks like:

[2025-12-01 14:05:01] Check complete. 0 posts published.
[2025-12-01 14:15:01] Check complete. 2 posts published.

If the log shows no entries for more than 15 minutes, the cron job has stopped. Set up a daily monitoring script that checks the log file's last modification time to catch failures early.

Shared Hosting Without Cron Access

Shared hosting environments that restrict custom cron jobs can use an external HTTP-based cron service (cron-job.org offers a free tier) to call /tools/cron_publish.php via HTTP. The script includes a token check: set CRON_SECRET in your environment file, and the script rejects requests that do not include the matching X-Cron-Secret header.

External Cron Service Options

  • cron-job.org — Free tier supports intervals down to 60 seconds with custom HTTP headers. Add a job targeting your cron URL with the X-Cron-Secret header.
  • UptimeRobot — Primarily a monitoring service, but its HTTP check triggers the cron endpoint as a side effect. Set the interval to 5 minutes.
  • n8n or Zapier — If you already use an automation platform, add a scheduled HTTP request node to centralise your infrastructure.

Practical Tips for Reliable Scheduling

  • Schedule posts at least 10 minutes into the future to account for cache delays and check intervals
  • Stagger multiple posts by 5 minutes rather than scheduling them at the exact same minute
  • Use the admin dashboard's "Scheduled Posts" widget to verify upcoming publications at a glance
  • Test your setup with a private post before relying on it for time-sensitive content
  • Each JekCMS site on the same server needs its own cron entry — they do not share the scheduling mechanism
  • The system respects timezones configured via the TIMEZONE constant in config/config.php