Skip to main content
← All use cases

Monitoring Laravel Forge Cron Jobs in Production

Forge shows a green check next to your server. The daemon list looks healthy. The scheduled jobs page still has the same * * * * * php /home/forge/app.com/artisan schedule:run entry you added two years ago. And yet last night’s invoice export never ran, the queue depth climbed overnight, and nobody knew until support tickets started landing at 9am.

Forge is excellent at provisioning cron and keeping supervisor processes alive. It is not a job-level monitor. If schedule:run exits 0 while individual tasks fail, stall, or never fire, Forge has nothing to show you.

How Forge Cron Actually Works

On a Forge-managed server, scheduling is two layers:

  1. System cron — Forge writes a real crontab entry (usually as the forge user) that runs every minute.
  2. Laravel’s scheduler — php artisan schedule:run evaluates your routes/console.php or app/Console/Kernel.php definitions and decides which tasks are due.

Forge also manages daemons (queue workers, Horizon, Reverb) through Supervisor. Those are long-running processes, not cron. Mixing the two in your mental model is how teams miss failures: a healthy daemon does not prove scheduled tasks ran.

What Forge can tell you:

What Forge cannot tell you:

Common Failure Modes on Forge

The crontab entry disappears or points at the wrong path

After a site rename, PHP version change, or manual server surgery, the Forge cron line can still point at an old release path or a removed PHP binary. Cron fails every minute. Forge UI still shows the old entry as if it were fine. You only notice when daily jobs stop.

Check the live crontab on the box:

sudo -u forge crontab -l
# expect something like:
# * * * * * cd /home/forge/app.com/current && php artisan schedule:run >> /dev/null 2>&1

If the path is wrong, fix it in the Forge UI (Scheduled Jobs) and redeploy confidence with a heartbeat (below).

schedule:run succeeds while tasks silently skip

Laravel’s scheduler can skip work without failing the process:

From Forge’s perspective the cron job “ran.” From the business’s perspective the digest never sent.

Daemon up, scheduler down

Teams monitor Horizon via Forge daemons and assume “background work is covered.” Cron is a separate pipeline. You can have healthy workers draining an empty queue while nightly model:prune, backup:run, and billing reconciliation never fire.

Multi-site / multi-server double-scheduling

Forge makes it easy to clone sites. Clone the server recipe and you often clone the same crontab onto two machines without onOneServer(). Jobs run twice — or, if they fight over locks, appear to run nowhere reliably.

Output discarded to /dev/null

The default Forge recipe often appends >> /dev/null 2>&1. Failures leave no local trail. By the time you SSH in, there is nothing to tail.

How to Monitor Forge Cron Jobs

1. Heartbeat the scheduler itself

Treat “did schedule:run fire this minute?” as a first-class signal. Add a lightweight scheduled task purely to prove the scheduler itself is alive:

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::call(fn () => null)
    ->everyMinute()
    ->name('scheduler-heartbeat')
    ->withoutOverlapping();

Crontinel’s package hooks into ScheduledTaskStarting, ScheduledTaskFinished, and ScheduledTaskFailed automatically once installed - no ping URL or config needed. Create a Cron monitor named scheduler-heartbeat with an alert window of a few minutes. If Forge’s crontab dies, PHP breaks after a version bump, or the release path is wrong, this monitor goes silent first - before the weekly report is late.

2. Let the package track critical artisan commands directly

Heartbeats prove the scheduler is alive. They do not prove backup:run finished. For high-value commands, no wrapping is needed - Crontinel’s package tracks every scheduled command automatically:

Schedule::command('backup:run')
    ->dailyAt('02:00')
    ->onOneServer()
    ->withoutOverlapping();

Create a matching Cron monitor for backup:run in the dashboard. If it exits non-zero, hangs past your grace period, or the run never starts at all, Crontinel reports it - no start/success/failure hooks to write or maintain.

3. Do not rely on Forge Heartbeats alone for Laravel schedules

Forge Heartbeats are useful for simple “did this URL get hit?” checks. Laravel schedules have richer failure modes: partial runs, mutex deadlocks, environment gates, and multi-server races. A single generic heartbeat is necessary but not sufficient for production billing, backups, and prune jobs.

Use Forge for server and daemon lifecycle. Use job-aware monitoring for schedule semantics.

4. Keep daemon and cron alerts separate

In Forge:

In your external monitor:

Practical Forge Checklist

  1. SSH in and confirm crontab -l path matches the current release directory after zero-downtime deploys.
  2. Confirm php -v on the cron line matches the site’s PHP version in Forge.
  3. Log scheduler output somewhere durable during incidents (temporary file or papertrail) instead of only /dev/null.
  4. After every major deploy, wait two minutes and confirm the external heartbeat still arrives.
  5. For multi-server apps, require onOneServer() plus a shared cache/Redis the lock can actually use.
  6. Alert on missed runs, not only on exceptions — most Forge cron pain is absence, not stack traces.

Where Crontinel Fits

Crontinel is built for the gap Forge leaves open: expected cadence, missed-run detection, and quiet failures inside Laravel’s scheduler. Point scheduled tasks (or the official Laravel SDK) at Crontinel, keep Forge managing servers and daemons, and you get paged when the job did not happen — not when the VM happens to be offline. The same logic applies if Forge is running your app under Octane: Octane serves HTTP, but it does not run the scheduler, so keep a separate heartbeat on schedule:run (why).

Install the Laravel package, attach monitors to schedule:run and your critical commands, and route alerts to the same Slack or PagerDuty channel your on-call already watches. Forge keeps the box alive. Crontinel watches the work.

See also

Start monitoring in minutes

Free for one app. No account needed to install and test locally.

composer require crontinel/laravel
php artisan crontinel:install
Get early access