What cron jobs do and why they matter for PHP applications
PHP applications often need tasks that run on a schedule. Sending batch emails, generating reports, cleaning up old data, syncing information from external services, and processing queued jobs are common examples. These tasks are too large or too time-consuming to run during a normal page request.
A cron job handles scheduled tasks on a Linux server by running a command at defined intervals. For a PHP application, that command typically calls a PHP script. The server executes the script in the background without involving a browser or a website visitor.
If you are running a VPS or a dedicated server and managing your own infrastructure, understanding how to set up and secure cron jobs is a practical skill. For context on monitoring server resources when these background tasks run, the article on server monitoring with htop and Netdata covers resource tracking in more detail.
How cron scheduling works on a Linux server
Cron reads a configuration file called a crontab. Each line in the crontab defines a schedule and a command to run. The format has five time fields followed by the command.
* * * * * command
| | | | |
| | | | └── Day of week (0-7, Sunday is both 0 and 7)
| | | └──── Month (1-12)
| | └────── Day of month (1-31)
| └──────── Hour (0-23)
└────────── Minute (0-59)
A practical example runs a PHP script every hour at the 15-minute mark:
15 * * * * /usr/bin/php /var/www/html/your-script.php
More specific schedules use ranges and lists. Running twice daily at 8am and 6pm looks like this:
0 8,18 * * * /usr/bin/php /var/www/html/daily-task.php
Running every five minutes uses a step value:
*/5 * * * * /usr/bin/php /var/www/html/frequent-task.php
For a deeper look at cron syntax, including examples for more complex schedules, the guide on cron jobs explained for Linux servers covers the full crontab format and common use cases.
Calling PHP scripts from cron
You need to decide how to invoke PHP. There are two common approaches.
Using the PHP binary directly
The straightforward approach calls the PHP interpreter and passes the script path:
0 * * * * /usr/bin/php /var/www/html/process-queue.php
Find the correct PHP path by running this command:
which php
The output shows the path, often /usr/bin/php or /usr/local/bin/php depending on the server setup.
Using a PHP one-liner with the -r flag
For very short commands, you can run inline PHP code without a script file:
*/15 * * * * /usr/bin/php -r "include '/var/www/html/bootstrap.php'; MyTask::run();"
This approach works for simple tasks but becomes hard to read for anything complex. Most background tasks benefit from a dedicated script file.
Setting the correct environment
Cron runs with a minimal environment. Variables like PATH and HOME may not be set the way your script expects. If your script uses relative paths or relies on environment variables, set them explicitly in the crontab:
PATH=/usr/local/bin:/usr/bin:/bin
HOME=/var/www
0 * * * * /usr/bin/php /var/www/html/your-script.php
Scripts that depend on configuration files should use absolute paths rather than assuming the working directory.
Securing PHP cron jobs
Background scripts often handle sensitive operations. Leaving them exposed or misconfigured creates risk. A few practical steps reduce that risk considerably.
Restrict script access
Place cron scripts outside the public web directory. If your website root is /var/www/html, put your background scripts in /var/www/includes/cron/ or a similar location that is not served by the web server.
Set appropriate file permissions. The script should be readable and executable by the cron user but not writable by others:
chmod 640 /var/www/includes/cron/your-script.php
chown www-data:www-data /var/www/includes/cron/your-script.php
Avoid exposing script URLs
If your cron script has a public URL that triggers it when accessed, anyone who discovers that URL can run it on demand. This is a common oversight.
Use a secret token as a parameter and validate it inside the script:
0 * * * * /usr/bin/php /var/www/includes/cron/report-generator.php --token=YourSecretToken123
In the PHP script, check the token before doing anything else:
if ($argv[1] !== '--token=YourSecretToken123') {
error_log('Unauthorized cron attempt');
exit(1);
}
For additional security layers, a review of your overall server management practices can help identify other hardening steps worth considering.
Limit which users can run cron
On systems with multiple users, the /etc/cron.deny and /etc/cron.allow files control who can schedule cron jobs. By default, cron is often open to all local users. Restricting it to the specific user that runs your web application is a sensible default.
Preventing concurrent execution
One of the most common problems with cron jobs is a task running more than once at the same time. If a script takes longer than the interval between cron runs, the next scheduled run starts before the previous one finishes. This can corrupt data, duplicate emails, or overwhelm your database.
File-based locking with flock
The simplest approach uses flock to lock a file before running the script. Cron starts the job only if the lock is available:
0 * * * * /usr/bin/flock -n /tmp/my-script.lock /usr/bin/php /var/www/html/my-script.php
The -n flag makes the lock non-blocking. If the script is already running, the command exits immediately without doing anything.
File locking inside PHP
For more control, implement locking directly in the PHP script using flock():
$lockFile = '/tmp/my-script.lock';
$fp = fopen($lockFile, 'c');
if (!flock($fp, LOCK_EX | LOCK_NB)) {
echo "Script is already running.\n";
fclose($fp);
exit(1);
}
try {
// Main script logic here
doWork();
} finally {
flock($fp, LOCK_UN);
fclose($fp);
}
The LOCK_EX flag acquires an exclusive lock. Adding LOCK_NB makes it non-blocking, so the script can detect and exit if it cannot obtain the lock.
Using a database lock
For distributed systems where multiple servers run the same cron job, a database lock is more reliable than a file lock. Store a lock record with a timestamp and check it before starting work:
$pdo = new PDO('mysql:host=localhost;dbname=yourdb', 'user', 'password');
$stmt = $pdo->prepare("SELECT id FROM cron_locks WHERE name = ? AND updated_at > DATE_SUB(NOW(), INTERVAL 1 HOUR)");
$stmt->execute(['process_queue']);
if ($stmt->fetch()) {
die("Lock is held by another process.\n");
}
$stmt = $pdo->prepare("INSERT INTO cron_locks (name, updated_at) VALUES (?, NOW()) ON DUPLICATE KEY UPDATE updated_at = NOW()");
$stmt->execute(['process_queue']);
// Do the work here
Logging and monitoring
Cron captures the output of a command and, by default, emails it to the system user. On a server without a configured mail system, output is often lost. Redirecting output to a file makes it possible to review what happened after the fact.
Redirecting output to a log file
0 * * * * /usr/bin/php /var/www/html/my-script.php >> /var/log/cron/my-script.log 2>&1
The >> appends output to the file. Adding 2>&1 redirects standard error to the same destination as standard output.
Create the directory and set appropriate permissions first:
mkdir -p /var/log/cron
touch /var/log/cron/my-script.log
chown www-data:www-data /var/log/cron/my-script.log
Structured logging in PHP
Inside your PHP script, use a logging approach that writes meaningful information. A simple error_log call works for basic output:
error_log("[" . date('Y-m-d H:i:s') . "] Processing started");
$processed = processQueue();
error_log("[" . date('Y-m-d H:i:s') . "] Processed $processed items");
For more robust logging, use a logging library or write to a structured format that is easy to parse and filter.
Monitoring cron health
A cron job that fails silently is worse than no cron job at all. Set up a check that alerts you when a job has not run successfully within an expected window.
One approach is to write a timestamp to a monitoring file after each successful run:
$monitorFile = '/var/www/includes/cron/.last-run';
file_put_contents($monitorFile, time());
Then use a separate monitoring script or external service to check the file's age and alert you if it has not been updated within the expected interval. If you are diagnosing performance issues on a VPS, checking how long these background tasks take to complete can help identify bottlenecks. The guide on VPS performance tuning and diagnosis covers server-side factors that affect task execution time.
Common mistakes and how to avoid them
- Relative paths breaking silently: Scripts that rely on the current working directory fail when cron runs them from a different location. Always use absolute paths for files and resources.
- Missing PHP extensions in CLI mode: The PHP version used by the web server and the version invoked by cron may use different configuration files. Check that the CLI php.ini has the extensions your script needs.
- Overlapping runs not handled: If a script takes longer than the cron interval, subsequent runs start before earlier ones finish. Always implement locking as described above.
- Scripts that do too much in one run: Processing thousands of records in a single cron run can timeout or exhaust memory. Break large tasks into smaller batches and iterate over them across multiple runs.
- Hardcoded credentials: Storing database passwords or API keys directly in the cron entry or script is a security risk. Use environment variables or a configuration file with restricted access instead.
- Ignoring return codes: Cron logs the exit status of commands. A script that exits with a non-zero code without logging why makes debugging difficult. Always log errors before exiting.
Deciding between cron and other scheduling methods
Cron is not always the right tool. Consider the complexity of your task and your infrastructure before committing to a cron-based approach.
Cron works well for straightforward, interval-based tasks that do not require complex scheduling, do not need to scale horizontally, and can tolerate some delay. A task that needs to run every hour, generate a report, and send it by email fits cron well.
A message queue system handles tasks that need to be processed as soon as possible, may need retry logic, or need to scale across multiple workers. If your application processes a high volume of background jobs, a queue-based approach with something like Beanstalkd, RabbitMQ, or a managed service may be more appropriate than cron.
For simple recurring tasks on a single server, cron remains the practical default. It is built into Linux, requires no additional software, and is straightforward to set up and maintain.
Setting up a first PHP cron job
If you have never configured a cron job before, here is the step-by-step process to set one up safely.
- Identify a task that needs scheduling. Look for operations that run during page requests but could run in the background instead, such as sending queued emails, generating static files, or cleaning expired sessions.
- Write the PHP script. Place it outside the public web directory. Start with basic logging to confirm the script runs.
- Test the script manually. Run it from the command line using the same PHP binary path that cron will use. Verify it completes without errors.
- Add the crontab entry. Open the crontab editor with
crontab -eand add your schedule and command with output redirected to a log file. - Check the log after the first run. Confirm the script ran at the expected time and that output appears in the log file.
- Monitor for the next few days. Watch the log file for any errors or unexpected behaviour. Adjust the schedule or script as needed.
Putting it together
Setting up cron jobs for PHP scripts is straightforward once you understand the basics of crontab syntax, environment handling, and output redirection. The parts that make the difference are the steps you take to keep those jobs secure, reliable, and observable.
A script that locks against concurrent runs, logs its activity, validates its inputs, and runs with restricted permissions is far more likely to behave predictably over the long term than one that just executes and relies on the schedule being correct. Build those habits from the start and you will spend less time troubleshooting silent failures.
If you need help reviewing your current cron setup, prepare a note with the crontab entries, the scripts involved, and the log file locations before getting in touch. That gives a clear starting point for any review or optimisation.