App Deployment·8 min read·

System Cron for wp-cron and Laravel Scheduler

Disable WP-Cron and Laravel web pings, then run wp-cron.php and artisan schedule:run from system cron so jobs fire on time without depending on visitor traffic.

NB

Netbay Developer Relations

Netbay Engineering

On this page

PHP frameworks love to pretend a web request is a clock. WordPress runs wp-cron.php when someone hits a page. Laravel has packages and old habits that fire the scheduler from a route. Both fail the same way: on a quiet site jobs wait for a visitor, on a busy site every request races to be the cron, and on a cached site the clock never runs at all. Production uses system cron. The web path is disabled.

This post sets DISABLE_WP_CRON, points WordPress at wp-cron.php from crontab, and runs artisan schedule:run every minute as www-data. Same Ubuntu 24.04 box, same PHP-FPM stack, Lucknow VPS.

Web hammer versus system cron Wrong: web hammer visitor request spawns wp-cron.php or hits /schedule quiet site = late jobs busy site = stampedes cached site = never Right: system cron crond every minute php wp-cron.php php artisan schedule:run user www-data CLI php.ini limits no HTTP needed Disable the web trigger first, then add crontab. Never both.

WordPress: disable WP-Cron, then add crontab

WP-Cron is a fake cron. On each request WordPress checks wp_options for due events and, if something is due, it spawns a loopback HTTP request to wp-cron.php. If you use a full-page cache, that spawn never happens. If you have a traffic spike, you get overlapping loopbacks. If nobody visits overnight, scheduled posts sit.

In wp-config.php, before the require of wp-settings.php:

javascript
define('DISABLE_WP_CRON', true);
define('ALTERNATE_WP_CRON', false);

Then install a system crontab. Prefer /etc/cron.d/wordpress so the job is a file you can deploy, not a surprise in crontab -e.

bash
cat >/etc/cron.d/wordpress << 'CRON'
SHELL=/bin/bash
PATH=/usr/sbin:/usr/bin:/sbin:/bin
*/5 * * * * www-data /usr/bin/php /var/www/wordpress/wp-cron.php >> /var/log/wp-cron.log 2>&1
CRON
chmod 644 /etc/cron.d/wordpress

Every five minutes is the usual WordPress interval. Every minute is fine if you have time-sensitive plugins; wp-cron.php is cheap when nothing is due. Run as www-data so log files and plugin temp files match FPM ownership. Do not run as root.

CLI php.ini applies here, not FPM. If a plugin needs 256M, set it in /etc/php/8.3/cli/php.ini or on the command line with php -d memory_limit=256M. Point the path at current/wp-cron.php if you use the git release layout.

Hit wp-cron.php over HTTP only if you must trigger from an external scheduler. Then lock it with a secret query argument and nginx allowlist. The local CLI path is simpler and does not depend on TLS or DNS.

Laravel: one crontab line, no web route

Laravel's scheduler is artisan schedule:run. It reads routes/console.php (or app/Console/Kernel.php on older apps) and runs due events. It is designed to be called every minute. It is not designed to be called from a Route::get('/cron').

bash
cat >/etc/cron.d/laravel << 'CRON'
SHELL=/bin/bash
PATH=/usr/sbin:/usr/bin:/sbin:/bin
* * * * * www-data cd /var/www/app/current && /usr/bin/php artisan schedule:run >> /var/log/laravel-schedule.log 2>&1
CRON
chmod 644 /etc/cron.d/laravel

cd to current so the shared .env and the symlink layout work. If you deploy in place, cd to /var/www/app. schedule:run on a quiet minute exits fast. On a due minute it runs the command; long work should be dispatched to the queue, not run inline in the scheduler process.

Remove any schedule:run from AppServiceProvider, from a livewire ping, from a health-check URL, and from kernel:schedule triggered by a package that hits the app over HTTP. Two clocks double-send invoices.

withoutOverlapping() on a schedule event needs cache. If CACHE_STORE is file and the cache dir is per-release, overlapping guards reset on deploy. Use database or redis cache for scheduler mutexes.

Logging, locks, and timezones

Log to /var/log/wp-cron.log and /var/log/laravel-schedule.log, owned by www-data, rotated by logrotate. Do not append forever on High-Speed SSD. A weekly rotate with copytruncate is enough.

Use flock if a job can run longer than the interval. WordPress plugins that process huge imports should take their own lock. Laravel withoutOverlapping is that lock. A second crontab line that wget http://127.0.0.1/wp-cron.php is how you get two overlapping clocks after you already added CLI cron.

Timezone: the VPS should be Asia/Kolkata for a Lucknow host, matching php.ini date.timezone. Cron uses the system timezone. If they disagree, scheduled posts fire an hour off and you will blame WordPress.

Do not disable systemd cron (cron.service on Ubuntu). Without it, /etc/cron.d files are decoration. systemctl enable --now cron.

Takeaway

Web-triggered cron is a traffic side effect, not a clock. DISABLE_WP_CRON plus php wp-cron.php, and artisan schedule:run from /etc/cron.d, give you a clock that runs when the site is cached, idle, or under load. Disable the hammer. Keep the crontab.

You can spin up an Ubuntu 24.04 instance on Netbay in under 60 seconds and put these cron files on Intel Xeon Platinum - netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance