Back to guides

Guides · April 18, 2026

PHP cron jobs: crontab, php-cli, and external cron (2026)

Four ways to schedule PHP on a clock: classic crontab with php-cli, long-running PHP daemons, framework schedulers like Laravel and Symfony, and external HTTP cron hitting your PHP-FPM stack. Each one trips over the same env-vs-cli pitfalls, mostly because the php in cron is a different binary loading a different ini than the one your phpinfo() page reports.
crontap.com / guides
Four ways to schedule PHP: crontab + php-cli, long-running daemons, framework schedulers (Laravel, Symfony), and external HTTP cron hitting a PHP-FPM endpoint. Plus the env-vs-cli pitfalls that bite every PHP cron.

You wrote a PHP script that regenerates the nightly sitemap, warms a Redis cache hourly, or exports a product feed at 4am. It works when you type php script.php, then silently fails under cron. The reason is almost always the same: the php in your shell is not the same binary cron sees, and the php.ini cron loads is not the one your phpinfo() reports.

PHP has four credible ways to schedule work on PHP 8.3 / 8.4: classic crontab + php-cli, a long-lived PHP daemon (Workerman, ReactPHP, Amphp, Swoole), a framework scheduler (Laravel, Symfony), or a small HTTP endpoint hit by external HTTP cron like Crontap.

Path 1: crontab + php-cli

The classic shape: a script on disk, one line in crontab -e, php from CLI.

# crontab -e
15 4 * * * /usr/bin/php /var/www/app/bin/export-feed.php >> /var/log/feed.log 2>&1

Three things make this brittle in PHP specifically:

  1. Use the absolute path to the PHP binary. which php in your shell may print /usr/local/bin/php (a Homebrew or update-alternatives symlink) while cron only sees /usr/bin/php. Different binary, different extensions, different php.ini.
  2. php-cli.ini is not php-fpm.ini. On Debian they live at /etc/php/8.3/cli/php.ini and /etc/php/8.3/fpm/php.ini with different defaults for memory_limit, max_execution_time, date.timezone, and loaded extensions. This is the single most common reason a script that "works in the browser" dies under cron. Run env -i /usr/bin/php -i | grep "Loaded Configuration" to see what cron loads.
  3. OPcache is off in CLI by default. Composer's autoloader hits disk on every invocation. Set opcache.enable_cli=1 for tight loops.

Path 2: Long-running PHP daemons

Workerman, ReactPHP, Amphp, and Swoole let you run PHP as a persistent worker with an in-process scheduler loop.

Be honest: PHP idiomatically is not a daemon language. Memory leaks in long-running PHP are a known footgun, and most teams pick these libraries for queue workers (Redis, RabbitMQ, Beanstalkd), not scheduling. If you already run a Workerman or Swoole worker, adding a scheduled tick is fine. If you do not, this is the wrong tool to introduce just for cron.

Path 3: Framework schedulers and managed hosts

Most modern PHP teams never touch raw crontab. They use a framework scheduler that resolves to one cron entry on the host.

  • Laravel Scheduler. Define jobs in app/Console/Kernel.php, then one cron line: * * * * * cd /var/www && php artisan schedule:run >> /dev/null 2>&1. See Laravel cron jobs.
  • Symfony Messenger + Scheduler. Symfony 6.3+ ships a Scheduler component wired to Messenger handlers.
  • cPanel cron. The UI most shared hosts ship, 1-minute floor. See cPanel cron jobs.
  • Managed PHP hosts (Cloudways, ServerPilot, Forge) wrap crontab with a friendlier UI.

Path 4: External HTTP cron hitting PHP-FPM

Skip cron on the box entirely. Add a small /cron/run endpoint that requires a bearer header and calls your work. The shape is the same in Slim, Mezzio, vanilla PHP, or a Laravel/Symfony controller.

// public/cron.php
$secret = getenv('CRON_SECRET');
$auth = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
if ($auth !== "Bearer {$secret}") {
    http_response_code(401);
    exit;
}
require __DIR__ . '/../src/jobs/export_feed.php';
http_response_code(204);

Point a Crontap schedule at POST https://yourapp.com/cron/run, attach Authorization: Bearer <secret>, pick a cron expression and IANA timezone. Pro is $3.25/mo annual flat for unlimited HTTP schedules at minute cadence on a 1-minute floor.

Schedule your PHP jobs from outside the box. Free forever tier with one schedule. Try Crontap →

Common PHP cron pitfalls

  • memory_limit cli vs fpm. A job that fits in 256M under FPM may hit the 128M CLI default.
  • max_execution_time = 0 in CLI. Unlimited. A loop forever piles up runs until the box OOMs. Wrap with timeout 600.
  • date.timezone differences. CLI may default to UTC while FPM has Europe/Berlin. Set it in both inis.
  • extension_dir mismatch. CLI may be missing intl, gd, imagick, or PDO drivers that work in FPM. Run php -m from a cron shell to verify.
  • $_SERVER and $_ENV not populated under cron. Use getenv() and vlucas/phpdotenv.
  • OPcache disabled in CLI. Composer autoloader perf takes a hit on every run.

FAQ

Why does my PHP script work manually but fail in cron?

Almost always one of three things: different php binary, different php.ini (CLI vs FPM), or missing env vars. Run env -i /usr/bin/php -i to see exactly what cron sees.

Should I use Laravel Scheduler or external cron for Laravel?

Scheduler is great inside one app. External HTTP cron wins when you want retries, Slack alerts, or scheduling that survives a redeploy. Many teams use both: scheduler for in-app ticks, Crontap for the heartbeat that wakes schedule:run. See Laravel cron jobs.

Can I schedule WordPress cron jobs this way?

Yes. Set define('DISABLE_WP_CRON', true); in wp-config.php, then hit wp-cron.php from Crontap. See Replace WordPress wp-cron and Cron jobs for WordPress.

What about long-running PHP jobs?

If a job exceeds your HTTP timeout, split it. The endpoint enqueues work into a queue (Redis, Beanstalkd, database) and a daemon worker (Path 2) processes it. Crontap fires the enqueue every minute; the work happens out-of-band.

Related on Crontap

From the blog

Read the blog

Guides, patterns and product updates.

Tutorials on scheduling API calls, webhooks and automations, plus deep dives into cron syntax, timezones and reliability.

Alternatives

Vercel Cron every minute: beating the Hobby hourly limit

Vercel Cron caps Hobby at hourly cadence and 5 jobs, and ties every change to a redeploy. Here is the external cron pattern teams use to ship per-minute schedules, per-IANA timezones, and one dashboard across projects without paying $20/mo per user for Pro.

Alternatives

Cloud Run cron without Cloud Scheduler

Cloud Scheduler costs $0.10 per job per month after the first 3 and asks for OIDC plus IAM bindings on every target. Here is the IAM-free pattern Cloud Run teams use to fire their .run.app URLs on a clock with one bearer token and one dashboard across every GCP project.