# How to Verify Laravel Scheduled Cron Jobs Are Running Correctly on Production

> Verify Laravel scheduled cron jobs run correctly in production. Ensure a single cron entry for schedule:run, use persistent cache for mutexes, and check logs or run with --verbose for confirmation.

- Repository: [Laravel/framework](https://github.com/laravel/framework)
- Tags: how-to-guide
- Published: 2026-02-20

---

**To verify Laravel scheduled cron jobs are running correctly on production, ensure your server has a single system cron entry invoking `php artisan schedule:run` every minute, configure a persistent cache driver for mutex locks, and inspect output logs or run the scheduler manually with `--verbose` to confirm due events execute.**

Laravel's task scheduler simplifies server automation by allowing you to define cron jobs in PHP rather than managing multiple system crontab entries. According to the `laravel/framework` source code, the scheduler relies on a single system cron that triggers the `schedule:run` Artisan command, which then evaluates and executes due tasks defined in your application's Console Kernel. Understanding this execution flow is essential for debugging production scheduling issues and ensuring critical background tasks complete reliably.

## How the Laravel Scheduler Works

The scheduler architecture consists of four core components that coordinate to run your tasks every minute.

### The System Cron Entry

The only server-level configuration required is a single crontab entry that invokes Laravel's Artisan dispatcher every minute. The system cron daemon executes:

```bash
* * * * * php /path/to/your/application/artisan schedule:run >> /dev/null 2>&1

```

This entry ensures `php artisan schedule:run` fires continuously, allowing Laravel to decide which tasks are due based on their defined frequencies.

### ScheduleRunCommand Execution

When the cron triggers, [`src/Illuminate/Console/Scheduling/ScheduleRunCommand.php`](https://github.com/laravel/framework/blob/main/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php) handles the request via its `handle()` method. This command resolves the `Schedule` instance and filters for due events:

```php
// src/Illuminate/Console/Scheduling/ScheduleRunCommand.php
$events = $this->schedule->dueEvents($this->laravel);
foreach ($events as $event) {
    // Execution logic...
}

```

The command iterates through due events and either runs them immediately or delegates to `runSingleServerEvent()` if **onOneServer** is enabled.

### Event Filtering and Due Checks

The `Schedule` class in [`src/Illuminate/Console/Scheduling/Schedule.php`](https://github.com/laravel/framework/blob/main/src/Illuminate/Console/Scheduling/Schedule.php) collects all defined events and filters them using `dueEvents()`:

```php
// src/Illuminate/Console/Scheduling/Schedule.php
public function dueEvents($app)
{
    return (new Collection($this->events))->filter->isDue($app);
}

```

Each `Event` object (defined in [`src/Illuminate/Console/Scheduling/Event.php`](https://github.com/laravel/framework/blob/main/src/Illuminate/Console/Scheduling/Event.php)) evaluates its Cron expression, environment constraints, and maintenance mode status through the `isDue()` method before being passed to the execution loop.

### Mutex and Overlap Protection

To prevent concurrent execution, the `Event` class implements **withoutOverlapping** protection via the `shouldSkipDueToOverlapping()` method:

```php
// src/Illuminate/Console/Scheduling/Event.php
public function shouldSkipDueToOverlapping()
{
    return $this->withoutOverlapping && ! $this->mutex->create($this);
}

```

For multi-server deployments, the `Schedule` class provides **onOneServer** logic through `serverShouldRun()` in [`src/Illuminate/Console/Scheduling/Schedule.php`](https://github.com/laravel/framework/blob/main/src/Illuminate/Console/Scheduling/Schedule.php), which creates a scheduling mutex ensuring only one server executes a given event at the scheduled time.

## Configuring Production Cron Jobs

Proper configuration requires synchronizing your system cron, application kernel, and cache infrastructure.

### System-Level Crontab Setup

Add the following entry to your server's crontab (edit via `crontab -e`):

```bash
* * * * * php /var/www/myapp/artisan schedule:run >> /dev/null 2>&1

```

Ensure the PHP binary path matches your application environment. Laravel resolves the binary automatically via `Application::phpBinary()` as implemented in [`src/Illuminate/Console/Application.php`](https://github.com/laravel/framework/blob/main/src/Illuminate/Console/Application.php).

### Defining Tasks in the Console Kernel

Define your scheduled tasks in [`app/Console/Kernel.php`](https://github.com/laravel/framework/blob/main/app/Console/Kernel.php) by overriding the abstract `schedule()` method from [`src/Illuminate/Foundation/Console/Kernel.php`](https://github.com/laravel/framework/blob/main/src/Illuminate/Foundation/Console/Kernel.php):

```php
use Illuminate\Console\Scheduling\Schedule;

protected function schedule(Schedule $schedule)
{
    $schedule->command('emails:send')
             ->dailyAt('02:00')
             ->withoutOverlapping()
             ->onOneServer()
             ->storeOutput();
}

```

The **withoutOverlapping** method prevents new instances from starting if a previous execution is still running, while **onOneServer** ensures the job runs on only one server in a horizontally scaled environment.

### Cache Driver Configuration

Both overlap protection and single-server execution require a persistent cache store. Set your driver in `.env`:

```env
CACHE_DRIVER=redis

# or specifically for scheduling:

SCHEDULE_CACHE_DRIVER=redis

```

Alternatively, force a specific store within your kernel using `useCache()`:

```php
$schedule->useCache('redis');

```

This ensures mutex locks survive across multiple `schedule:run` invocations and server restarts.

## Verification and Monitoring Strategies

Proactive monitoring prevents silent failures in production environments.

### Manual Testing with Verbose Output

Test your schedule definition manually by running:

```bash
php artisan schedule:run --verbose

```

This outputs "Running [command]" for each due event, allowing you to verify that Cron expressions evaluate correctly and constraints (environments, maintenance mode) permit execution.

### Inspecting Logs and Output

By default, scheduled tasks discard output to `/dev/null`. Enable logging using **storeOutput()** or **sendOutputTo()**:

```php
$schedule->command('backup:run')
         ->daily()
         ->storeOutput();

```

Logs are stored at `storage_path('logs/schedule-'.sha1($event->mutexName()).'.log')` by default. Review these files to verify successful completion or capture error messages.

### Checking Mutex Cache Keys

Verify that locks are being created and released correctly:

- **File-based cache**: Inspect `storage/framework/cache/data/illuminate:schedule:*`
- **Redis**: Check for keys matching `framework:schedule-{sha1}` using `redis-cli KEYS 'framework:schedule*'`

If these keys persist after a task completes, the mutex may not be clearing properly, indicating a potential crash or timeout during execution.

### Building Health Check Endpoints

Expose a monitoring endpoint to report upcoming scheduled runs:

```php
Route::get('/schedule/next', function (Schedule $schedule) {
    return $schedule->events()
        ->map(fn ($e) => [
            'command' => $e->getSummaryForDisplay(),
            'next'    => $e->nextRunDate()->toDateTimeString(),
        ]);
});

```

This allows external monitoring systems to verify that tasks remain scheduled and calculate expected execution times.

## Handling Failures and Debugging

Implement failure detection to respond to missed or failed executions.

### Failure Callbacks and Notifications

Attach callbacks to handle failures gracefully:

```php
$schedule->command('backup:run')
         ->daily()
         ->onFailure(fn () => Log::error('Backup job failed'))
         ->emailOutputOnFailure('admin@example.com');

```

The `onFailure()` callback executes immediately when a command returns a non-zero exit code, while `emailOutputOnFailure()` sends the captured output to specified recipients.

### Clearing Stale Cache

If you modify schedule definitions while the configuration is cached, clear the scheduling mutex cache:

```bash
php artisan schedule:clear-cache

```

This removes stale entries from `CacheEventMutex` and `CacheSchedulingMutex` implementations, preventing "ghost" locks from blocking new executions.

## Summary

- **System Cron**: A single crontab entry running `php artisan schedule:run` every minute drives the entire scheduling system.
- **Mutex Configuration**: Enable `withoutOverlapping` and `onOneServer` only after configuring a persistent cache driver (Redis recommended) via `CACHE_DRIVER` or `SCHEDULE_CACHE_DRIVER`.
- **Verification**: Use `schedule:run --verbose` for manual testing, inspect `storage/logs/schedule-*.log` for output, and monitor Redis keys matching `framework:schedule-*` for lock status.
- **Source Files**: The core logic resides in [`src/Illuminate/Console/Scheduling/ScheduleRunCommand.php`](https://github.com/laravel/framework/blob/main/src/Illuminate/Console/Scheduling/ScheduleRunCommand.php), [`src/Illuminate/Console/Scheduling/Schedule.php`](https://github.com/laravel/framework/blob/main/src/Illuminate/Console/Scheduling/Schedule.php), and [`src/Illuminate/Console/Scheduling/Event.php`](https://github.com/laravel/framework/blob/main/src/Illuminate/Console/Scheduling/Event.php).
- **Cleanup**: Use `php artisan schedule:clear-cache` when deploying schedule changes to prevent mutex conflicts.

## Frequently Asked Questions

### How do I check if my Laravel cron job is actually running?

Run `php artisan schedule:run --verbose` manually to see which events are due and whether they execute. Additionally, check your configured log files (via `storeOutput()`) or implement an `onSuccess` callback that writes to your application logs. If using `onOneServer`, verify the cache keys exist in your Redis or file cache to confirm a server has claimed the lock.

### What cache driver should I use for Laravel scheduled tasks in production?

Use **Redis** or **Memcached** for production environments. File-based caching works for single-server deployments but can cause race conditions on multi-server setups. Set `CACHE_DRIVER=redis` in your `.env` file, or specify a dedicated driver via `SCHEDULE_CACHE_DRIVER` if your application cache uses a different backend.

### Why is my Laravel scheduler running the same job multiple times?

This occurs when `onOneServer()` is not used in multi-server deployments, or when the cache driver is misconfigured (e.g., using the `array` driver which is not persistent). Ensure your cache driver supports atomic locks and that all servers share the same cache instance. Check [`src/Illuminate/Console/Scheduling/CacheSchedulingMutex.php`](https://github.com/laravel/framework/blob/main/src/Illuminate/Console/Scheduling/CacheSchedulingMutex.php) for the locking implementation details.

### Where are Laravel scheduled task logs stored?

By default, logs are not stored unless you call `storeOutput()` or `sendOutputTo($path)` on the event. When using `storeOutput()`, logs are written to `storage/logs/schedule-{hash}.log` where the hash is derived from the event's mutex name. You can specify a custom path using `sendOutputTo('/custom/path.log')` on your scheduled task definition.