How to run a PHP script as a cron job
To run a PHP script on a schedule, add a line to your crontab with crontab -e: the five schedule fields, then the full path to php and to the script, as in */5 * * * * /usr/bin/php /var/www/app/cron.php. Cron runs the PHP command line, not your web server, so the script gets a different environment from the one you test in the browser.
Five things that trip up PHP cron jobs
Cron needs the full path to php
Cron'sPATHis short. Find the binary withcommand -v phpand write it out, along with the script's full path.- Settings and extensions you see in
phpinfo()on the web may differ from what the command line loads. Check withphp --ini. The working directory is your home
Relative paths inrequireorfopenbreak. Build them from__DIR__.Errors vanish unless you log them
Redirect the output to a file, or cron mails it to an inbox nobody reads.A slow run can overlap the next
On the command linemax_execution_timedefaults to 0, so nothing stops a stuck script. Wrap it inflock.
Add the job
First run the script by hand from the command line, exactly as cron will: /usr/bin/php /var/www/app/cron/send-emails.php. If it fails there, it will fail in cron. Then open your crontab with crontab -e, add the line and save. The job runs as the user whose crontab it is, so pick the user that owns the files the script writes, often www-data: sudo crontab -u www-data -e.
On shared hosting with cPanel or Plesk, there's no shell: the control panel's Cron Jobs page asks for the schedule and the command separately. The command is the same, with the PHP path your host lists.
Use the full path to php
Cron doesn't read your shell profile, and its PATHis short (Debian's cron uses /usr/bin:/bin), so a php installed in /usr/local/bin or by a version manager isn't found. Run command -v php in your terminal and copy the result into the crontab. A server with several PHP versions has a binary for each, such as /usr/bin/php8.3; name the version your app needs. A % in the command line, as in date +%F, must be written \%, or cron cuts the command there.
The command line has its own php.ini
PHP-FPM and Apache's module load one configuration; the php command loads another. On Debian and Ubuntu they live in /etc/php/8.3/fpm and /etc/php/8.3/cli. Run php --ini to see which file the command line reads, and php -m to list its extensions. A missing extension or a different memory_limit is the usual reason a script works in the browser and fails from cron. On the command line max_execution_time defaults to 0, no limit.
Don't rely on the working directory
Cron starts each job in the user's home directory, not the script's folder, so require 'config.php' looks for ~/config.php. Build paths from __DIR__, the directory of the file itself, or put cd /var/www/app && before the command.
$_SERVER['HTTP_HOST'], $_GETand sessions don't exist on the command line. Pass options as arguments instead (they arrive in $argv), and check PHP_SAPIso a cron script can't be run from a browser:
<?php
// Refuse to run from a browser
if (PHP_SAPI !== 'cli') {
http_response_code(403);
exit;
}
// Paths relative to this file, not to cron's working directory
require __DIR__ . '/../vendor/autoload.php';
$config = require __DIR__ . '/../config.php';
echo date('c'), " sending queued emails\n";Log the output
Whatever the script prints, warnings and fatal errors included, goes to cron, which mails it to the crontab's owner, and on most servers that mail goes nowhere. Append >> /var/log/app/job.log 2>&1 to keep both output and errors in a file. To confirm cron started the job at all, read its log: journalctl -u cron on Debian and Ubuntu, journalctl -u crond on Fedora and RHEL.
Stop runs from overlapping
Cron starts the next run on time even if the last one is still going. A job every 5 minutes that once takes 7 ends up running twice at the same time, sending emails twice or locking the same rows. Put /usr/bin/flock -n /tmp/job.lock in front of the command: it holds a lock while the script runs, and -n makes the next run exit at once if the lock is taken. Inside PHP, flock() on a file you keep open does the same.
Laravel, Symfony and WordPress
Laravel keeps its schedule in code and needs one cron entry that runs schedule:run every minute; the framework decides which tasks are due:
* * * * * cd /var/www/app && /usr/bin/php artisan schedule:run >> /dev/null 2>&1Symfony's Scheduler component runs as a long-lived worker (messenger:consume) rather than from cron, though many Symfony apps still call console commands from crontab the same way.
WordPress's built-in WP-Cron isn't cron: it runs due tasks when someone visits the site, so a quiet site runs them late. Turn it off in wp-config.php and run the due events from real cron with WP-CLI:
# wp-config.php
define( 'DISABLE_WP_CRON', true );
# crontab: run due WordPress events every 5 minutes
*/5 * * * * /usr/local/bin/wp --path=/var/www/html cron event run --due-now >> /var/log/wp-cron.log 2>&1Run it through a URL instead
If you can't run php from cron at all, cron (or an outside cron service) can request a page with curl. The script then runs in the web server with the web php.ini and its time limits, and anyone who finds the URL can run it, so require a secret:
*/15 * * * * /usr/bin/curl -fsS "https://example.com/cron.php?key=LONG_RANDOM_SECRET" > /dev/nullCommon schedules for PHP jobs
| Expression | Runs | Typical job |
|---|---|---|
* * * * * | Every minute. | Laravel and Symfony schedulers, queue checks |
*/5 * * * * | Every 5 minutes. | Sending queued email, WordPress events |
*/15 * * * * | Every 15 minutes. | Syncing with an API, clearing a cache |
0 * * * * | Every hour, on the hour. | Imports, sitemaps, cleanup |
30 2 * * * | At 02:30 every day. | Reports and backups, when traffic is low |
0 6 * * 1 | At 06:00 on Mondays. | A weekly summary email |
Times are the server's
date.timezone setting changes what date() prints, not when cron starts the job.