PHP Cron Jobs: The Practical Guide
Running PHP from cron is straightforward once you know that the command line uses a different configuration from your web server.
Updated 5 August 2026 · 6 min read
-
Written by
Andrian Valeanu
Founder of Pulsetic
-
Reviewed by
Ionut Caval
Technical reviewer
Call the PHP CLI binary by absolute path: 0 3 * * * /usr/bin/php /var/www/app/task.php >> /var/log/task.log 2>&1. The CLI uses its own php.ini, separate from PHP-FPM, so limits and extensions can differ from what your site uses. Laravel needs one entry running schedule:run every minute. WordPress needs WP-Cron disabled and a real cron entry in its place.
Key takeaways
- PHP CLI reads a different php.ini from PHP-FPM. Check with
php --ini. max_execution_timeis 0 on the CLI, so a long job will not be cut short the way a web request would be.- Laravel takes exactly one crontab entry, no matter how many scheduled tasks you define.
- WP-Cron is triggered by visitor traffic, not by time. On a quiet site, scheduled events simply do not fire.
- Never expose a job script inside your public web root.
The basic crontab entry
Point cron at the PHP CLI binary and the script, both by absolute path, and send the output somewhere you can read it.
0 3 * * * /usr/bin/php /var/www/app/scripts/nightly.php >> /var/log/nightly.log 2>&1
# Find your CLI binary if you are unsure
which php
php --version
On servers with several PHP versions installed the generic php may not be the one your application expects, so pin the version explicitly: /usr/bin/php8.3 rather than /usr/bin/php.
The CLI is not your web server
This trips people up regularly. PHP-FPM and the command line each load their own configuration file, so a script that behaves one way through the browser can behave differently from cron on the very same machine.
php --ini
# Loaded Configuration File: /etc/php/8.3/cli/php.ini
# The web server is using a different one:
# /etc/php/8.3/fpm/php.ini
| Setting | Typical CLI value | Why it matters |
|---|---|---|
max_execution_time | 0 (unlimited) | A long job will not be killed the way a web request would be |
memory_limit | -1 (unlimited) on many builds | Large batch jobs succeed where a web request would exhaust memory |
display_errors | Often on | Errors go to standard output, so your redirect captures them |
| Extensions | May differ from FPM | An extension enabled for the site may be missing on the CLI |
If a script works in the browser but not from cron, compare php --ini against your FPM configuration before assuming the problem is cron.
Laravel: one entry, whatever the schedule
Laravel handles its own scheduling internally. Cron's only job is to wake the scheduler every minute so it can decide what is due, which means you add exactly one crontab line and never touch it again, however many tasks you define in the application.
* * * * * cd /var/www/app && /usr/bin/php artisan schedule:run >> /dev/null 2>&1
From Laravel 11 onwards the tasks themselves live in routes/console.php; in earlier versions they are in the schedule() method of the console kernel. Either way the crontab side is identical.
Discarding the output here is deliberate and normal, because the scheduler runs every minute and would otherwise produce a great deal of noise. Log the individual tasks instead, and check what is due with php artisan schedule:list.
WordPress: WP-Cron is not cron
This one is worth understanding properly, because the name actively misleads. WP-Cron is not time-based at all. It runs when somebody visits the site: WordPress checks on each page load whether any scheduled event is overdue and, if so, fires it during that request.
The consequences follow directly. On a site with little traffic, scheduled posts publish late and backups do not run overnight, because nobody visited to trigger them. On a busy site the opposite problem appears, with the check adding overhead to visitor requests. Neither is what you want from a scheduler.
// wp-config.php: stop WordPress firing cron on page loads
define('DISABLE_WP_CRON', true);
# Then schedule it properly, every 5 minutes
*/5 * * * * /usr/bin/php /var/www/html/wp-cron.php >> /var/log/wp-cron.log 2>&1
# Or with WP-CLI, which gives clearer output
*/5 * * * * cd /var/www/html && /usr/local/bin/wp cron event run --due-now >> /var/log/wp-cron.log 2>&1
Disabling WP-Cron without adding a real cron entry stops scheduled events from running at all. The two changes belong in the same deploy.
Write the schedule without counting fields
The free cron expression generator builds the five fields and previews the next run times.
Keep job scripts out of the web root
A script placed in /var/www/html/ so that cron can find it is also reachable at https://yoursite.com/ by anyone who guesses the filename. Maintenance scripts frequently have no authentication, because their author assumed only cron would ever run them.
Put them outside the document root, in something like /var/www/app/scripts/, and let cron call them by absolute path. If a script genuinely has to sit in the web root, require a secret token and deny it in your server configuration.
Running as the right user
A PHP job that writes cache or upload files should run as the same user as your web server, usually www-data or nginx. Run it as root and it will create files the web server cannot later modify, which produces permission errors that appear long after the cron job that caused them.
sudo crontab -e -u www-data
Knowing the job still runs
PHP scheduled work tends to be the kind nobody watches: queue cleanup, invoice generation, feed imports, backups. Cron does not check exit codes and reports nothing when a job fails, so a task that has been throwing a fatal error since a dependency update looks exactly like one that is working.
Have the job report in when it finishes. With cron job monitoring, a run that does not check in on time raises an alert, which is the only way to catch a Laravel scheduler that stopped because the crontab was lost in a server migration.
See how Pulsetic's cron job monitoring catches this from the outside, across 15+ locations.
Frequently asked questions
-
How do I run a PHP script with cron?
Use the absolute path to the PHP CLI binary and the script: "0 3 * * * /usr/bin/php /var/www/app/task.php >> /var/log/task.log 2>&1". Find the binary with "which php", and pin the version explicitly if the server has several installed.
-
Why does my PHP script work in the browser but not from cron?
The command line loads a different php.ini from PHP-FPM, so limits and enabled extensions can differ. Run "php --ini" to see which file the CLI uses, and compare it with the FPM configuration.
-
How many cron entries does Laravel need?
One. A single entry running "php artisan schedule:run" every minute wakes the scheduler, which then decides which of your defined tasks are due. Adding more tasks in the application never requires another crontab line.
-
Is WP-Cron the same as a real cron job?
No. WP-Cron is triggered by visitors loading pages, not by time, so on a quiet site scheduled events run late or not at all. Set DISABLE_WP_CRON to true in wp-config.php and call wp-cron.php from a real crontab entry instead.
-
Catch the next outage before your visitors do.
2-minute setup · Cancel any time
-
No credit card needed