
September 07, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
Choosing between a Laravel cron job vs queue worker when to use each is one of the first architectural decisions you make on any production app that sends email, syncs data, or runs reports. Both run code outside the HTTP request cycle, but they solve different problems: the scheduler fires tasks on a clock, while queue workers process jobs as they arrive. Get this wrong and you either miss nightly backups or block a checkout while a PDF generates. This guide compares both paths with real config, deployment patterns from apps I've maintained, and a clear decision framework for modern Laravel architecture.
What is the difference between Laravel cron jobs and queue workers?
Laravel does not define a separate "cron job" class. In practice, developers mean the task scheduler: a single system cron entry that runs php artisan schedule:run every minute, which then evaluates scheduled tasks defined in routes/console.php (Laravel 11+) or app/Console/Kernel.php (Laravel 10 and earlier). Queue workers are long-running PHP processes—managed by Supervisor, systemd, or Laravel Horizon—that pull jobs from Redis, database, or SQS and execute them immediately.
The mental model is simple. Cron answers when something should run. Queues answer what should run outside the current request without making the user wait. On a legal-tech portal I built, booking confirmation emails go through a queue; a nightly command that archives expired document links runs on the scheduler. Same framework, different trigger.
Scheduler: one cron entry, many tasks
Your server needs exactly one crontab line. Laravel evaluates which scheduled tasks are due within that minute:
# /etc/cron.d/laravel-app (run as the deploy user)
* * * * * cd /var/www/myapp/current && /usr/bin/php artisan schedule:run >> /dev/null 2>&1 Tasks are registered in routes/console.php on Laravel 12 and 13:
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:daily-sales')
->dailyAt('01:00')
->timezone('Asia/Kathmandu')
->withoutOverlapping()
->onOneServer();
Schedule::command('cache:prune-stale-tags')->hourly(); The scheduler supports closures, queued jobs (more on that below), shell commands, and task output logging. Methods like withoutOverlapping() prevent a slow report from stacking if yesterday's run is still going—a common production gotcha I've seen on under-provisioned VPS hosts.
Queue workers: persistent job processors
When a controller dispatches a job, Laravel serialises it to the configured queue connection. A worker process dequeues and executes it:
// app/Jobs/SendBookingConfirmation.php
class SendBookingConfirmation implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $timeout = 120;
public function __construct(public Booking $booking) {}
public function handle(MailService $mail): void
{
$mail->sendConfirmation($this->booking);
}
}
// In controller
SendBookingConfirmation::dispatch($booking); Workers must be running continuously. On Ubuntu servers I maintain, Supervisor keeps them alive:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=/usr/bin/php /var/www/myapp/current/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=deploy
numprocs=4
redirect_stderr=true
stdout_logfile=/var/www/myapp/shared/storage/logs/worker.log For Redis-backed queues at scale, Horizon replaces raw Supervisor configs with a dashboard, balancing, and metrics. See the dedicated guide on scaling Laravel queues for high-traffic applications.
| Criteria | Laravel Scheduler (Cron) | Queue Workers |
|---|---|---|
| Trigger | Time (every minute, daily, weekly) | Event (user action, webhook, model observer) |
| Process model | Short-lived; runs and exits | Long-lived; polls for new jobs |
| Latency | Up to 60 seconds (cron granularity) | Milliseconds to seconds after dispatch |
| Concurrency | withoutOverlapping(), onOneServer() | Multiple workers, Horizon auto-scaling |
| Retries | Manual; re-run on next schedule | Built-in $tries, backoff, failed_jobs table |
| Best for | Reports, cleanup, renewals, sync windows | Email, SMS, PDFs, API calls, imports |
| User waiting? | No user involved | User triggered; response must be fast |
When should you use Laravel scheduled tasks instead of queues?
Reach for the scheduler when the work is tied to a calendar, not a user click. If the task must run whether or not anyone visited the site today, it belongs on the schedule.
- Periodic maintenance — pruning old sessions, clearing expired password reset tokens, rotating log files.
- Scheduled reports — daily sales summaries emailed to admins at 6 AM Nepal time (
Asia/Kathmandu). - Subscription and billing cycles — charge recurring plans on the 1st of each month.
- Data sync windows — pull inventory from a supplier API every four hours during off-peak hours.
- Health checks — ping external services, verify SSL expiry, alert if disk usage exceeds 85%.
- Cache warming — pre-build sitemap or homepage fragments before morning traffic.
A pattern I use on eCommerce projects like Nepal Gift Card: the scheduler marks expired gift cards at midnight; the queue sends the purchase confirmation the moment checkout completes. Mixing these up means either customers wait 60 seconds for an email or expired cards stay purchasable until the next cron tick.
Scheduled tasks that dispatch jobs
Laravel lets you schedule a job class directly—useful when the same job is both user-triggered and periodically re-run:
use App\Jobs\SyncInventoryFromSupplier;
use Illuminate\Support\Facades\Schedule;
Schedule::job(new SyncInventoryFromSupplier)->everyFourHours(); The scheduler still decides when; the job still runs through the queue infrastructure. You need workers running for this to work. The cron entry only enqueues—the worker executes.
When cron alone is enough
Skip the queue entirely if the task is lightweight, idempotent, and completes in seconds:
Schedule::command('model:prune', ['--model' => 'App\\Models\\AuditLog'])
->daily()
->runInBackground(); runInBackground() spawns a subprocess so schedule:run itself does not block other scheduled tasks. Do not use this for heavy work—spawn a queued job instead so retries and failure tracking apply.
When should you use Laravel queue workers instead of cron?
Any work triggered by a user, webhook, or domain event that would slow the HTTP response belongs on a queue. If removing the job makes the page feel instant, queue it.
- Notifications — transactional email, SMS, push, Slack alerts after order placement or form submission.
- Third-party API calls — payment verification, shipping label creation, CRM sync. Timeouts here kill checkout flows.
- File and media processing — image resizing, PDF generation, video transcoding. Spatie Media Library conversions are a classic queue candidate; see the Spatie Media Library guide.
- Webhook delivery — your app calling a client's endpoint with retry/backoff logic.
- Bulk imports — CSV uploads parsed row-by-row via chained or batched jobs. For large batches, read about Laravel job batching.
- Search index updates — Meilisearch or Elasticsearch indexing after model save.
On Adventure Third Pole Trek, booking confirmation, supplier notification, and payment callback handling all dispatch to Redis queues. The Livewire booking form returns in under 300 ms because nothing waits on SMTP or Khalti API latency.
Queue configuration essentials
Set your default connection in .env and match it in production:
QUEUE_CONNECTION=redis
REDIS_CLIENT=phpredis
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379 Redis 8.10 is the current anchor version; it handles queue throughput far better than the database driver, which polls MySQL 9.7 and creates lock contention under load. Use the database driver only on shared hosting where Redis is unavailable—accept the performance trade-off.
Define retry behaviour on the job class, not globally:
public int $tries = 5;
public array $backoff = [30, 60, 120, 300, 600];
public function failed(Throwable $exception): void
{
Log::error('Booking confirmation failed', [
'booking_id' => $this->booking->id,
'error' => $exception->getMessage(),
]);
} Failed jobs land in the failed_jobs table. Monitor it. A silent queue failure on a payment webhook is worse than a loud HTTP 500.
How do you configure Laravel cron and queue workers in production?
Production setup spans three layers: OS cron, process supervision, and deployment hooks. I've deployed this stack across sister sites sharing a Linux system administration pipeline with Deployer 7 and GitLab CI.
Step 1: Register the system cron entry
One line in the deploy user's crontab—or a file under /etc/cron.d/ on Ubuntu 22/24:
* * * * * deploy cd /var/www/myapp/current && php artisan schedule:run >> /var/www/myapp/shared/storage/logs/scheduler.log 2>&1 Verify it works:
php artisan schedule:list
php artisan schedule:test On Laravel 13 with PHP 8.3+, ensure the cron uses the same PHP binary as PHP-FPM. A version mismatch between cron (8.2) and FPM (8.5) causes cryptic class-not-found errors after deploy. This is the single most common scheduler failure I've debugged on client servers.
Step 2: Run queue workers under Supervisor
Install Supervisor, add the program config, reload:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:* After each Deployer release, workers must restart to pick up new code. Add to your deploy recipe:
task('artisan:queue:restart', function () {
run('{{bin/php}} {{release_path}}/artisan queue:restart');
}); queue:restart sets a cache flag; workers finish their current job then exit. Supervisor respawns them against the new release symlink. Without this step, workers run stale opcached code until manually killed—a silent post-deploy bug.
Step 3: Use Horizon for Redis queues at scale
When you run more than two worker processes or need visibility into throughput, install Horizon. It replaces manual Supervisor worker configs with a single horizon process and gives you a dashboard at /horizon (protect it in production).
Horizon handles worker balancing, memory limits, and graceful termination. Pair it with the patterns in the Laravel Horizon monitoring guide.
Monitoring both paths
Scheduler health: log task output and alert on missed runs. Laravel 11+ supports ->emailOutputOnFailure('ops@example.com') per task. Queue health: watch queue depth, failed job count, and worker uptime. A stalled queue on a payment-heavy app like one using Khalti payment integration means confirmed orders with no confirmation email—a support ticket avalanche.
For server-level cron concepts beyond Laravel, the Ubuntu cron jobs guide covers crontab syntax and timezone pitfalls that affect schedule:run.
How do Laravel cron jobs and queue workers work together?
Mature Laravel 12/13 applications use both. The scheduler orchestrates; the queue executes heavy or retryable units of work. Treat them as complementary, not competing.
Real-world combined pattern
On a production Laravel application with PostgreSQL 18 and Redis 8.10, a typical daily cycle looks like this:
- 00:05 — Scheduler runs
subscriptions:renew. For each due subscription, the command dispatchesProcessRenewalPaymentto thepaymentsqueue. - 01:00 — Scheduler runs
reports:daily, which queries aggregates and dispatches oneGenerateReportPdfjob per tenant. - Every hour —
Schedule::job(new PurgeExpiredSessions)keeps the session table lean. - On every booking — Controller dispatches
SendBookingConfirmation,NotifySupplier, andUpdateSearchIndexto separate queues for isolation.
Queue names (payments, notifications, default) let you assign dedicated workers. A slow PDF job must not block payment processing.
Anti-patterns to avoid
Cron polling the database for work. A scheduled command that runs every minute, selects rows where status = pending, and processes them is a queue antipattern. You lose retry semantics, create race conditions with multiple servers, and hammer the database. Dispatch a job when the row is created; let workers process it.
Running heavy work inside scheduled commands without a queue. A daily import that takes 20 minutes will overlap tomorrow's run unless withoutOverlapping() blocks it—and then you miss a day. Chunk the import into batched jobs.
Using sync queue driver in production. QUEUE_CONNECTION=sync executes jobs inline during the request. Fine for local development; catastrophic for checkout flows in production.
Forgetting onOneServer() in multi-server setups. Without it, three app servers each run the same scheduled task. Three duplicate daily reports, three renewal charges. Add onOneServer() and a shared cache driver (Redis) so Laravel acquires a mutex.
For database design that supports high-volume queued workloads, see the PostgreSQL for Laravel developers guide. Queue payloads and failed job records grow fast; index and prune them.
Local development parity
Run the scheduler in dev with:
php artisan schedule:work Process queues locally with:
php artisan queue:listen --tries=1 Or use composer dev scripts that run server, queue listener, and Vite 8.x concurrently. Match production queue drivers in staging—debugging a Redis-specific serialisation bug on the database driver locally wastes hours.
If you need help auditing an existing app's background job architecture, Laravel support and maintenance covers scheduler review, queue tuning, and Horizon setup. For greenfield apps, enterprise application development includes queue design from day one.
Key Takeaways
- Use the Laravel task scheduler (one OS cron entry) for time-based work: reports, cleanup, renewals, and sync windows—not for reacting to user clicks.
- Use queue workers for event-driven, retryable jobs: email, payments, webhooks, file processing, and anything that would slow an HTTP response.
- Most production Laravel 12/13 apps need both: the scheduler decides when; queues handle how heavy work executes with retries.
- Always restart workers after deploy (
queue:restart), match PHP versions between cron and PHP-FPM, and use Redis over the database queue driver when possible. - Add
withoutOverlapping()andonOneServer()to scheduled tasks; define$triesand$backoffon job classes. - Monitor failed jobs and scheduler logs—silent queue failures are harder to diagnose than loud HTTP errors.
People Also Ask
Can Laravel run queue workers without cron?
Yes. Queue workers are independent long-running processes started by Supervisor or Horizon. They do not require cron. However, if you schedule jobs with Schedule::job() or run any scheduled tasks, you still need the single schedule:run cron entry. User-dispatched jobs need only workers.
Should scheduled tasks use the database or Redis queue?
Prefer Redis for production queue throughput and lower latency. The database driver works on budget shared hosting but adds polling overhead to MySQL 9.7 or MariaDB 12.3. Scheduled tasks that dispatch jobs inherit whichever QUEUE_CONNECTION your app uses—set it in .env and keep staging aligned with production.
What happens if queue workers stop but cron keeps running?
Scheduled tasks that dispatch jobs will enqueue work, but nothing processes it. Jobs pile up in Redis or the jobs table. User-triggered dispatches also stall. Supervisor's autorestart=true handles crashes; monitor worker uptime and queue depth. Horizon sends notifications when wait times exceed thresholds.
Is Laravel Horizon required for queue workers?
No. Raw queue:work under Supervisor is sufficient for small and mid-size apps. Horizon adds a dashboard, auto-balancing, and per-queue worker configuration—worth it once you run four or more workers or need visibility into job throughput. Official docs cover both approaches at laravel.com/docs/queues and laravel.com/docs/scheduling.
Pick the right tool and ship reliable background work
The Laravel cron job vs queue worker when to use each question boils down to trigger type: clock versus event. Schedule periodic orchestration; queue everything that users or webhooks initiate and that needs retries. Wire both into your deploy pipeline, monitor failures, and test with production queue drivers before launch. Background job architecture is not an afterthought—it is what keeps checkout fast, emails delivered, and nightly reports running while you sleep.
Need a production audit of your scheduler and queue setup, or help scaling workers on Ubuntu? Contact us for a review. Explore more Laravel guides on the blog, validate JSON queue payloads with the JSON formatter, or read about Kokil Thapa's background building production systems since 2010. For new builds, see web development services and related work in the portfolio.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

