All posts

Cron Job PHP: The Complete Guide to Running It Right

September 14, 2026

Running a cron job PHP script sounds trivial until the job that "works fine" on your machine does nothing — or fails halfway through — on the server, with no error surfacing anywhere. This covers the two execution methods, the environment differences that break scripts silently, how framework schedulers like Laravel's fit into the raw crontab model, and why exit code 0 tells you almost nothing about whether your job actually succeeded.

Two Ways to Run a PHP Script from Cron

There are exactly two ways to run a PHP script from cron, and picking the wrong one causes most confusion.

The first is direct CLI invocation: cron calls the PHP binary and passes your script as an argument, e.g. php /path/script.php. This runs the script via php-cli, no web server involved — the standard approach on a VPS or dedicated server where you control the PHP installation.

The second is HTTP-trigger invocation: cron runs curl or wget against a URL, and your web server executes the script as it would for a normal visitor. This is common on shared hosting or webhook-style setups where a hosting panel restricts direct CLI access, or where the script depends on being bootstrapped through your application's front controller.

The CLI approach is faster, avoids web server overhead, and gives cleaner access to exit codes. The curl/wget approach is sometimes unavoidable on constrained hosting, but it introduces extra failure points — DNS issues, SSL handshakes, HTTP timeouts — a local CLI call never faces. If you have shell access, prefer the CLI invocation over hitting a URL. Cronitor's guide to PHP cron jobs and hosting.com's breakdown of curl vs direct interpreter calls cover this fork in more detail.

The Crontab Line: PHP Binary Path, Script Path, and Common Mistakes

A correct, working example:

* * * * * /usr/bin/php /home/user/app/scripts/task.php >> /home/user/app/logs/task.log 2>&1

Every piece matters. Cron runs with a minimal environment and no shell profile, so it doesn't know your interactive shell's PATH. Both the PHP binary and the script need absolute paths — php script.php often fails under cron even though it works when typed manually, because cron can't find php.

On shared hosting with cPanel or Plesk, this gets messier: servers frequently ship multiple PHP versions (/usr/bin/php, /usr/local/bin/php7.4, /opt/cpanel/ea-php81/root/usr/bin/php), and the version cron picks by default may not match what your application needs. crongenerator.dev's PHP cron job guide confirms this is the single most common setup mistake — run which php or check your hosting panel's cron interface for the exact binary path rather than assuming.

The redirection at the end (>> logfile 2>&1) isn't optional if you want visibility: without it, cron mails output to the account owner by default (often nowhere useful) or discards it entirely, leaving zero record of what happened.

Why PHP Behaves Differently Under Cron Than in a Browser

A script that runs perfectly in a browser can fail — or do nothing — under cron, almost always due to the difference between the CLI SAPI and web SAPI. PHP loads a separate php.ini for CLI execution, meaning different memory_limit, different enabled extensions, and sometimes a different error_reporting level. A script relying on an extension only enabled in the web-facing php.ini will fail under cron with no obvious explanation unless you check both config files.

Beyond configuration, CLI execution has no $_SERVER['HTTP_HOST'], no request context, and no assumed working directory — cron typically starts in the user's home directory, not your project root. A script using relative file paths (require 'config.php') will break under cron even though it worked in the browser, simply because the current directory differs. Understanding what actually happens when cron executes a command — the stripped-down environment, missing variables, minimal shell — makes these failures far less mysterious.

Framework Schedulers: Laravel, Symfony, and WordPress Patterns

If you're running Laravel, you don't need a crontab entry per scheduled task. Laravel's task scheduler uses a single cron line that runs every minute and delegates everything else to your application:

* * * * * cd /path/to/project && php artisan schedule:run >> /dev/null 2>&1

schedule:run checks your routes/console.php or Console/Kernel.php definitions and executes whatever is due at that minute — daily jobs, hourly jobs, custom cron expressions — all from that one entry. This pattern is documented directly in the Laravel 12.x scheduling docs: one system cron entry, unlimited application-level schedules.

Symfony doesn't ship an equivalent built-in scheduler, so raw crontab entries invoking individual bin/console commands remain the norm, though bundles exist for centralizing schedules similarly.

WordPress takes a different approach: wp-cron runs as a pseudo-cron triggered by site visits rather than the system clock, so low-traffic sites can see scheduled tasks delayed or skipped. Disabling wp-cron in favor of a real system cron entry hitting wp-cron.php directly is the standard fix where reliability matters.

The Silent Failure Problem: Exit Code 0 Doesn't Mean Success

Cron judges success by exit code alone, which is where PHP cron jobs get dangerous. A fatal error, an uncaught exception, or a warning silenced by @ error suppression can all let a script terminate with exit code 0 — cron sees a "successful" run even though nothing useful happened. PHP doesn't automatically map internal errors to non-zero exit codes; that mapping only exists if your code explicitly calls exit(1) or throws an exception handled that way at the entry point.

Timeouts compound this: a script that hangs past a max_execution_time limit or gets killed by the server may leave partial output, a half-written file, or a corrupted database state, all while cron logs show nothing unusual. Cron has no concept of correctness — it only knows whether a process returned and what code it returned with. That distinction between "ran" and "worked" is exactly the gap explored in checking whether a cron job actually ran and succeeded.

Monitoring Your PHP Cron Jobs

The fix for the exit-code-0 problem is external verification, not more error handling inside the script. Two patterns work well: have your script ping a monitoring endpoint immediately after it finishes successfully, or have a monitor independently hit the script's URL/health-check and alert you when a run is late, missing, or returns a failure signal.

Writing a curl or php artisan schedule:run line is easy. Knowing whether it actually did anything useful three weeks later, at 3 a.m., is the hard part — and that's exactly the gap Cronevra fills. It tracks expected run times, flags missed executions and failures, and alerts your team before a silent PHP cron failure turns into a bigger problem. Set it up once at Cronevra, and check the pricing page when you're ready to monitor more than a handful of jobs.

Frequently Asked Questions

Do I need curl or can I run a PHP script directly from cron?

You can run PHP scripts directly from cron using the CLI binary (php /path/script.php), the preferred method when you have shell access since it avoids web server overhead and HTTP-layer failure points. Curl or wget is only necessary on hosting setups that restrict CLI execution or when the script must run through your web app's front controller.

Why does my PHP cron job work manually but not on schedule?

The most common cause is environment differences: cron uses a minimal shell with no PATH, a different working directory, and often a separate php.ini than your interactive session or web server. Absolute paths for both the PHP binary and script, plus explicit working-directory handling in code, resolve most of these mismatches.

How do I find the correct PHP binary path for my crontab entry?

Run which php from your server's shell, or check your hosting control panel's cron job interface, which often lists available PHP versions explicitly. On shared hosting with multiple PHP versions installed (common with cPanel and Plesk), the default php command may not point to the version your application actually needs.

Does Laravel need a separate cron entry for every scheduled task?

No — Laravel needs only one crontab entry running php artisan schedule:run every minute, and the framework's internal scheduler handles dispatching all defined tasks at their configured times. This is documented directly in Laravel's task scheduling docs and is the standard pattern for any Laravel application.

Can a PHP cron job fail silently even if it shows as completed?

Yes — a script can encounter a fatal error or uncaught exception and still return exit code 0 unless that error is explicitly caught and mapped to a non-zero code. Cron only records the exit code, so it has no way to distinguish a genuinely successful run from one that failed internally but exited cleanly.

How can I monitor a PHP cron job for failures or missed runs?

The reliable approach is external monitoring: have the script ping a monitoring service on successful completion, or have a monitor check the job's expected schedule and alert when a run is missing or late. Cronevra provides this monitoring layer specifically for catching missed runs and silent failures in scheduled PHP jobs.