All posts

Crontab PHP: The Correct Syntax and Why Jobs Fail Silently

August 31, 2026

Running a PHP script through crontab looks trivial until the job that worked in your terminal starts failing at 3 a.m. with no error, no output, and no clue why. This walks through the exact syntax, the three PHP-specific reasons cron jobs go brittle, and how to know when one fails.

The Correct Crontab Syntax for Running a PHP Script

Open your crontab with crontab -e. This opens the current user's crontab in your default editor — the right way to add or edit scheduled jobs rather than hand-editing files in /etc/cron.d.

A cron line has five time fields followed by the command: minute, hour, day of month, month, day of week. To run a PHP script every hour on the hour:

0 * * * * /usr/bin/php /var/www/myapp/scripts/task.php >> /var/www/myapp/logs/task.log 2>&1

That's the whole pattern: absolute path to the PHP binary, absolute path to the script, then redirection for output. Skip the schedule fields and cron won't accept the entry; skip the absolute paths and you'll hit the failures covered below.

Finding the Right PHP Binary Path

Never assume php on its own, or even /usr/bin/php, is the binary cron will actually use. Cron runs jobs with a minimal environment and a sparse PATH — often just /usr/bin:/bin — so it may not resolve php the same way your interactive shell does. On systems with multiple PHP versions installed side by side (common on shared hosting and anywhere PHP-FPM runs alongside CLI), php might point to 7.4 in one place and 8.2 in another. Homebrew on macOS adds another layer, since it symlinks versioned binaries and updates the target on upgrade.

Find the exact path cron should use with:

which php

If you manage multiple versions, check more specifically:

command -v php8.2

Whatever that command returns is the absolute path to paste into your crontab line — not php, not a relative assumption. This one lookup prevents most "command not found" errors before they happen.

Three Things That Make PHP Cron Jobs Break

A syntactically correct crontab line still fails for reasons that have nothing to do with cron and everything to do with how PHP behaves outside a browser context.

CLI php.ini diverges from FPM php.ini. The PHP binary running under Apache or PHP-FPM often loads a different php.ini than the CLI SAPI — different memory_limit, different error_reporting, sometimes different extensions. A script that runs fine through a web request can behave differently, or crash outright, under cron. Confirm which config file is in play with:

php -i | grep "Loaded Configuration File"

Compare that path against what your web server reports through phpinfo(). If they differ, that's your bug. The PHP manual on command-line usage covers how the CLI SAPI is configured and how php_sapi_name() lets you detect which context you're in.

Cron strips environment variables. Your shell's .bashrc or .profile exports things like database credentials, API keys, or a customized PATH — none of which cron loads, since cron jobs don't run in a login shell. A script that reads getenv('DB_PASSWORD') will get false under cron even though it works when run manually. Diagnose it by dumping cron's actual environment to a file:

* * * * * env > /tmp/cron-env.txt

Compare that output against your normal shell's env. The fix is sourcing an env file explicitly at the top of the script, exporting the needed variables directly in the crontab with VAR=value lines above your jobs, or loading a .env file through your framework's config loader before anything else runs.

Relative paths break includes and autoloaders. Cron's working directory is not your project root — typically it's the home directory of whatever user owns the crontab. A script that does require '../vendor/autoload.php' or reads a config file with a relative path works fine when you cd into the project and run it by hand, then fails under cron because that path resolves somewhere else entirely. Confirm this by adding pwd as a temporary diagnostic line, or just use dirname(__FILE__) to build absolute paths for every include and file read.

Testing and Logging Output

Before trusting cron with a script, run the identical command cron will use, in a fresh shell, exactly as written in the crontab line — same absolute paths, same redirection:

/usr/bin/php /var/www/myapp/scripts/task.php >> /var/www/myapp/logs/task.log 2>&1

The >> appends stdout to a log file, and 2>&1 redirects stderr into the same stream. Without that second redirect, PHP errors and warnings vanish instead of reaching your log. Once the job is live, tail the log after the first few scheduled runs to confirm it's writing what you expect — not just that the file exists, but that it contains the output a successful run should produce.

When PHP Cron Jobs Fail Silently

Here's the uncomfortable part: even with correct paths, correct environment variables, and logging in place, PHP cron jobs still fail in ways that produce zero visible signal. A fatal error can print to stderr, but if your redirection setup is wrong or the error occurs before output buffering flushes, nothing lands in the log. A script that hits its memory_limit exits without a stack trace in some configurations. An uncaught exception can terminate the script while the wrapping shell command still returns exit code 0, so anything watching exit codes sees "success." None of these trigger an email, a Slack message, or anything else — the job just stops doing its job, and the first person to notice is usually a customer.

Grepping log files after the fact tells you a job failed yesterday. It doesn't tell you a job failed twenty minutes ago and is actively causing damage right now. For a deeper look at how cron actually schedules and executes jobs under the hood, see Linux Cron Explained, and for detection patterns, Crontab Monitoring: How to Catch Silent Cron Failures covers catching failures before they compound.

PHP's specific failure modes — swallowed fatals, memory exhaustion, exit codes that lie — make it a particularly bad candidate for "I'll notice if something's wrong." Add a heartbeat ping to the script or to the wrapper command in your crontab line, so a missed run, a slow run, or a failed run triggers an alert instead of a support ticket. That's the entire premise behind Cronevra: cron jobs that never fail silently, with a setup that takes about as long as writing the crontab line itself.

Frequently Asked Questions

Why does my PHP script work in the browser but fail when run from cron?

The CLI SAPI often loads a different php.ini than your web server's SAPI, with different memory limits, error reporting, or extensions. Cron also runs with a stripped environment and a different working directory, so environment variables and relative file paths that work in a browser request may not resolve the same way. Check php -i | grep "Loaded Configuration File" from both contexts to confirm.

Do I need to use the full path to php in crontab?

Yes — cron uses a minimal PATH that often doesn't include the same directories your interactive shell does, so a bare php command can fail with "command not found." Always use the absolute path returned by which php in your crontab line.

How do I find the correct PHP binary path for cron?

Run which php in your terminal, or command -v php8.2 if you have multiple versions installed. Use whatever absolute path that returns directly in your crontab entry rather than relying on the bare command name.

Can I pass environment variables to a PHP script run by cron?

Yes, either by adding VAR=value lines directly above your jobs in the crontab file, or by sourcing an environment file at the top of the script before anything else executes. Cron does not automatically load your shell's .bashrc or .profile, so any variables your script depends on need to be set explicitly.

Should I use curl to hit a PHP script's URL or run it directly with php-cli in cron?

Running the script directly with php-cli is generally more reliable, since it avoids depending on the web server being up, adds no HTTP overhead, and lets you capture stdout/stderr directly through shell redirection. Hitting a URL with curl only makes sense if the script genuinely needs the web SAPI's context or routing.

How do I get error output from a PHP cron job instead of it failing silently?

Redirect both stdout and stderr in your crontab line using >> /path/to/log 2>&1, which captures everything PHP would otherwise discard. That only solves logging, though — for actual failure notification when a job stops running or exits with a hidden error, you need a monitoring layer like a heartbeat ping that alerts you the moment a run is missed or fails.