How to Set Up Cron Jobs in Laravel (Task Scheduling Guide)
Learn how to set up cron jobs in Laravel using the built-in task scheduler. Step-by-step guide covering schedule definitions, testing, deployment, and debugging.
Prerequisites
- Laravel 10+ (or Laravel 11+)
- PHP 8.1+
- Access to server or hosting with cron support
- Composer installed
In this guide
What Is Laravel Task Scheduling?
Laravel's task scheduler lets you define all your scheduled commands in a single place — your application code — instead of managing individual crontab entries on your server. You only need one system cron entry that runs every minute, and Laravel handles the rest.
This means your schedule is version-controlled, easy to test locally, and doesn't require SSH access to modify.
The scheduler supports a fluent API for defining when tasks run:
// Laravel 11+: routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('emails:send')->dailyAt('08:00');
Schedule::command('reports:generate')->weeklyOn(1, '9:00');
Schedule::job(new CleanupOldRecords)->daily();Under the hood, Laravel checks every minute which tasks are due and runs them.
Prerequisites
Before you start, make sure you have:
- Laravel 10+ (or Laravel 11+ for the simplified console routing)
- PHP 8.1+
- A server where you can add a crontab entry (shared hosting, VPS, Docker, or a PaaS like Forge/Vapor)
- Basic familiarity with Artisan commands
If you're using Laravel 11+, schedules are defined in routes/console.php. For Laravel 10 and earlier, you'll use the schedule method in app/Console/Kernel.php.
Quick Start: Your First Scheduled Task
Step 1: Define a scheduled task
For Laravel 11+, open routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('inspire')->hourly();For Laravel 10 and earlier, open app/Console/Kernel.php:
protected function schedule(Schedule $schedule): void
{
$schedule->command('inspire')->hourly();
}Step 2: Add the system cron entry
On your server, run crontab -e and add this single line:
* * * * * cd /path-to-your-project && php artisan schedule:run >> /dev/null 2>&1This tells cron to run Laravel's scheduler every minute. This might seem excessive, but it's by design — Laravel's scheduler manages timing internally. Each minute, schedule:run checks which tasks are actually due based on your fluent schedule definitions (->hourly(), ->dailyAt('09:00'), etc.) and only executes those. The every-minute cron is just the heartbeat that lets Laravel's scheduler do its job.
Step 3: Verify it works
php artisan schedule:listThis shows all registered scheduled tasks and when they'll next run.
Scheduling Methods Deep Dive
Laravel provides a rich API for scheduling. Here are the most common methods:
Frequency methods:
->cron('* * * * *')— raw cron expression->everyMinute()— run every minute->everyFiveMinutes()— run every 5 minutes->hourly()— run at the start of every hour->hourlyAt(15)— run at minute 15 of every hour->daily()— run daily at midnight->dailyAt('13:00')— run daily at 1 PM->weekly()— run weekly on Sunday at midnight->weeklyOn(1, '8:00')— run weekly on Monday at 8 AM->monthly()— run on the first of each month->quarterly()— run on the first day of each quarter->yearly()— run on January 1st
Constraint methods:
Schedule::command('report:daily')
->daily()
->weekdays()
->between('8:00', '17:00')
->when(fn () => config('app.env') === 'production')
->skip(fn () => app()->isDownForMaintenance());Overlap prevention:
Schedule::command('analytics:process')
->hourly()
->withoutOverlapping()
->onOneServer()
->runInBackground();Common Laravel Cron Expressions
When using the ->cron() method directly, here are common expressions:
->cron('*/5 * * * *')— Every 5 minutes->cron('0 * * * *')— Every hour->cron('0 0 * * *')— Daily at midnight->cron('0 9 * * 1-5')— Weekdays at 9 AM->cron('0 0 1 * *')— First day of every month->cron('0 0 * * 0')— Every Sunday at midnight
Most developers prefer the fluent API (->daily(), ->hourly(), etc.) since it's more readable, but the ->cron() method is useful for non-standard schedules.
Every weekday at 9:00 AM
Next runs (UTC):
Mon, May 18, 2026 09:00
Tue, May 19, 2026 09:00
Wed, May 20, 2026 09:00
Testing Schedules Locally
Laravel provides several commands for testing your schedules without waiting for the actual cron to fire:
List all scheduled tasks:
php artisan schedule:listShows each task, its frequency, next due date, and description.
Run the scheduler once:
php artisan schedule:runExecutes any tasks that are currently due. Useful for one-off testing.
Run the scheduler continuously (local dev):
php artisan schedule:workRuns the scheduler in the foreground, checking every minute. Perfect for local development — no need to set up a real crontab.
Test a specific task:
php artisan schedule:testShows a list of scheduled tasks and lets you pick one to run immediately, regardless of its schedule.
Deployment: Setting Up the System Cron
The exact setup varies by hosting environment:
Ubuntu / Debian:
crontab -e
# Add this line:
* * * * * cd /var/www/html && php artisan schedule:run >> /dev/null 2>&1Docker:
In your Dockerfile or entrypoint:
RUN echo "* * * * * cd /var/www/html && php artisan schedule:run >> /proc/1/fd/1 2>&1" | crontab -Laravel Forge:
Forge automatically adds the scheduler cron entry when you deploy a Laravel app. No manual setup needed.
Laravel Vapor (serverless):
Vapor uses AWS CloudWatch Events to trigger the scheduler. Define it in your vapor.yml:
environments:
production:
scheduler: trueDebugging: "Why Is My Laravel Schedule Not Running?"
Common causes and fixes:
1. The system cron entry is missing or wrong
Verify with crontab -l. The path to your project and PHP binary must be correct. Use absolute paths:
* * * * * /usr/bin/php /var/www/html/artisan schedule:run >> /dev/null 2>&12. The schedule is defined but not registering
Run php artisan schedule:list. If your task isn't listed, check that you're defining it in the right file (routes/console.php for Laravel 11+, Kernel.php for Laravel 10).
3. Timezone mismatch
Laravel defaults to UTC. Set your app timezone in config/app.php:
'timezone' => 'America/New_York',Or set it per-task:
Schedule::command('report:daily')
->dailyAt('09:00')
->timezone('America/New_York');4. Overlapping prevention is blocking runs
If you use ->withoutOverlapping(), a stuck or long-running task can prevent future runs. Check for stale mutex files in storage/framework/schedule-*.
5. Output is being swallowed
Log output to a file to see errors:
Schedule::command('emails:send')
->daily()
->appendOutputTo(storage_path('logs/schedule.log'));Production Tips
Logging and monitoring:
Schedule::command('billing:charge')
->daily()
->onSuccess(fn () => Log::info('Billing completed'))
->onFailure(fn () => Log::error('Billing failed'))
->emailOutputOnFailure('ops@example.com');Health checks:
Use a monitoring service like Oh Dear or Healthchecks.io:
Schedule::command('important:task')
->hourly()
->pingOnSuccess('https://hc-ping.com/your-uuid')
->pingOnFailure('https://hc-ping.com/your-uuid/fail');Maintenance mode:
By default, scheduled tasks don't run during maintenance mode. To override:
Schedule::command('heartbeat:ping')->everyMinute()->evenInMaintenanceMode();Multi-server deployments:
Use ->onOneServer() with a cache driver that supports locks (Redis, Memcached, DynamoDB) to prevent duplicate runs across servers.