Cron with PHP: The Three Correct Ways to Schedule Scripts
August 11, 2026


What "Cron with PHP" Actually Means
Cron doesn't know what PHP is. It's a scheduler that fires a command line at a given time — nothing more. "Running cron with PHP" means pointing that command line at the php binary, which then executes a script or bootstraps a framework command. A PHP cron job isn't a special cron feature; it's an ordinary cron entry whose payload happens to be PHP code.
That means every PHP scheduling problem is really one of two problems: either the crontab line is wrong, or the PHP process it launches fails in a way cron never sees. This tutorial walks through the three real ways to structure that command line, gives a working example of each, and covers the part most tutorials skip — confirming the job did what it was supposed to, not just that it ran.
Three Ways to Put PHP on a Schedule
There are three legitimate architectures for running a PHP script on a schedule, and picking the right one up front saves debugging later.
1. Raw crontab calling php-cli directly. Cron invokes php with your script as an argument. No intermediary, no framework — just a PHP CLI cron job doing one thing. Right for standalone scripts, small maintenance tasks, or projects with no framework scheduler.
2. A shell wrapper script. Cron calls a .sh script that sets environment variables, changes into the correct working directory, adds logging, and then calls php itself. This is the pragmatic middle ground — you control the environment PHP runs in, which raw crontab entries can't do on their own.
3. A framework-native scheduler. Laravel's Task Scheduling and Symfony's console-command approach both use a single crontab entry that triggers the framework, which then decides internally what actually needs to run. You still need cron under the hood — the framework just centralizes logic that would otherwise live in dozens of separate crontab lines.
None of these is universally "correct." A one-off backup script doesn't need Laravel's scheduler pulled in; a Laravel app with fifteen scheduled tasks shouldn't have fifteen separate crontab entries either.
Example: A Correct Crontab Line for a Plain PHP Script
For a standalone script, the safest crontab entry uses absolute paths for both the PHP binary and the script:
* * * * * /usr/bin/php /var/www/app/scripts/send-digest.php >> /var/log/send-digest.log 2>&1
Three details make this line correct rather than accidentally-working: the full path to php-cli (found with which php), the full path to the script itself, and output redirection so stdout and stderr land somewhere readable. The most common crontab php mistake is writing a bare php script.php — cron runs jobs with a minimal environment and no shell profile, so it often can't resolve php from PATH. That's the single biggest reason a PHP cron job is not running even though the identical command works fine in a terminal. If you're chasing that symptom, the PHP Crontab Setup Guide covers path resolution and permission issues in more depth.
Example: Cron with a Framework Scheduler (Laravel)
If you're on Laravel, you don't hand-write a crontab line per task. You write one line, and Laravel's scheduler decides what runs and when. The current recommendation from the Laravel 12.x Task Scheduling docs is:
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1
This fires every minute, but schedule:run checks your routes/console.php (or scheduler class) and only executes tasks that are actually due. That's the whole answer to how schedule:run fits into a normal crontab — it needs exactly one entry that runs constantly and defers the real decision-making to the framework. If you're deploying on managed infrastructure like Laravel Forge, this entry is typically configured for you automatically.
Symfony doesn't ship an identical scheduler, but the pattern is equivalent: define a console command, then drive it with a comparable crontab entry (or a systemd timer) calling bin/console app:your-command. Either way, the framework decides what runs — cron just becomes the heartbeat that keeps checking in.
Why a "Successful" Cron Run Can Still Be a Failed Job
Cron logs — and Laravel's own scheduler output — tell you the process launched and exited. They don't tell you the underlying task actually worked. A PHP cron job silent failure happens when the script starts, hits an unhandled exception mid-way, catches it, logs nothing, and still exits with code 0. Cron sees exit code 0 and considers its job done. Your database update, API call, or file write never happened, and nothing surfaced that fact anywhere you'd naturally look.
This gap is outside the scope of "how to schedule a task," so framework documentation rarely addresses it. Laravel's and Symfony's docs show the happy path — task defined, task runs. Neither tells you what to do when a task silently stops updating a table it's supposed to update every night. For deeper reliability practices once your scheduling mechanics are settled, Cron Jobs in Production: How to Keep Them Reliable is the natural next read.
Closing the Gap: Monitoring Your PHP Cron Jobs
The fix is a ping, not more logging. Your job calls a monitoring URL when it starts, another when it finishes successfully, and the monitor raises an alert if neither ping arrives on schedule — a dead man's switch for your scheduled task. This works whether your PHP job is a raw script, a shell-wrapped process, or a task inside Laravel's or Symfony's scheduler, because you're adding a heartbeat around the job, not changing what it does.
Cronevra is built around exactly that pattern. Wrap your existing php script.php or artisan schedule:run command with a Cronevra ping call — no rewrite, no new framework, no migration off crontab — and you get cron job alerts for PHP tasks that stop reporting in, run long, or exit with a failure code. If your "PHP cron job" is really an internal service hitting an HTTP endpoint on a timer rather than a CLI script, the same monitoring logic applies; see the HTTP Request Scheduler guide for that variant specifically.
The Actual Gap to Close
Cron and framework schedulers are honest about one thing only: the process launched. Whether it succeeded — the email sent, the row updated, the file written — is a separate question that neither crontab nor schedule:run answers on its own. Wrapping your existing PHP cron command with a heartbeat check is the fastest way to close that gap without touching your job's logic. Cronevra adds that visibility in minutes, and the pricing page has the details if you're ready to stop guessing whether last night's job actually did its job.
Frequently Asked Questions
What does it actually mean to run "cron with PHP"?
It means a cron entry whose scheduled command happens to invoke the PHP binary or a framework console command. Cron has no PHP-specific behavior — it just executes a command line at set intervals, and that command line is what makes it a "PHP cron job."
What's the difference between calling php directly from crontab and using a wrapper script?
Calling php directly runs your script with cron's minimal environment and default working directory. A shell wrapper script runs first, letting you set environment variables, change directories, and add logging before it calls PHP — more control at the cost of one extra file to maintain.
How does schedule:run fit into a normal crontab?
You add exactly one crontab entry that runs php artisan schedule:run every minute; Laravel checks internally which defined tasks are actually due and runs only those. The crontab entry never changes even as you add or remove scheduled tasks inside the application.
What is the one crontab line needed for Laravel/Symfony scheduling?
For Laravel: * * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1, per the current Laravel docs. Symfony has no identical built-in scheduler, but the equivalent pattern is a crontab or systemd timer entry calling a defined console command every minute.
Why does a PHP cron job show as "ran" in logs but still not do its job?
Because cron logs and exit codes confirm the process launched and exited without a fatal error — they don't verify the business logic inside the script actually completed. A script can catch an exception, log nothing useful, and still exit with code 0, leaving the intended task silently undone.