Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Cron Job vs Queue Worker When to Use Each

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.

Laravel Background ProcessingTask Scheduler (Cron)Time-based triggersschedule:run every minuteArtisan commands + closuresQueue WorkersEvent-driven jobsLong-running processesRedis / DB / SQS driverHTTP RequestController dispatches Job to queueScheduler runs command on schedule
Laravel cron job vs queue worker: scheduler runs on a clock, workers react to dispatched jobs

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.

CriteriaLaravel Scheduler (Cron)Queue Workers
TriggerTime (every minute, daily, weekly)Event (user action, webhook, model observer)
Process modelShort-lived; runs and exitsLong-lived; polls for new jobs
LatencyUp to 60 seconds (cron granularity)Milliseconds to seconds after dispatch
ConcurrencywithoutOverlapping(), onOneServer()Multiple workers, Horizon auto-scaling
RetriesManual; re-run on next scheduleBuilt-in $tries, backoff, failed_jobs table
Best forReports, cleanup, renewals, sync windowsEmail, SMS, PDFs, API calls, imports
User waiting?No user involvedUser 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.
When to Use the Scheduler?New background taskMust run at fixed time?Daily / hourly / weeklyYESUse SchedulerArtisan commandNOUser triggered?Check queue nextUse Queue Worker
Decision tree: fixed schedule points to Laravel cron; user events point to queue workers

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.

  1. Notifications — transactional email, SMS, push, Slack alerts after order placement or form submission.
  2. Third-party API calls — payment verification, shipping label creation, CRM sync. Timeouts here kill checkout flows.
  3. 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.
  4. Webhook delivery — your app calling a client's endpoint with retry/backoff logic.
  5. Bulk imports — CSV uploads parsed row-by-row via chained or batched jobs. For large batches, read about Laravel job batching.
  6. 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.

Queue Job LifecycleDispatchControllerRedisQueue storeWorkerqueue:workCompleteOr retryFailure PathException thrown → release with backoffMax tries exceeded → failed_jobs tableHorizon / logs alert ops teamfail
Laravel queue worker lifecycle: dispatch, store, process, retry or record failure

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.

Scheduler + Queue TogetherOS Cron: * * * * * schedule:runEvery minute on production serverLaravel Scheduler evaluates tasks01:00 daily report | hourly cache | every 4h syncLight commandRuns inlinecache:pruneSchedule::job()Dispatches to queueSyncInventory jobHTTP dispatchUser checkoutSendEmail jobSupervisor / Horizon workers process all queued jobs
Production pattern: Laravel cron orchestrates timing; queue workers handle execution for heavy tasks

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 dispatches ProcessRenewalPayment to the payments queue.
  • 01:00 — Scheduler runs reports:daily, which queries aggregates and dispatches one GenerateReportPdf job per tenant.
  • Every hourSchedule::job(new PurgeExpiredSessions) keeps the session table lean.
  • On every booking — Controller dispatches SendBookingConfirmation, NotifySupplier, and UpdateSearchIndex to 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() and onOneServer() to scheduled tasks; define $tries and $backoff on 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

Laravel cron means the task scheduler: one system cron entry runs schedule:run every minute, which evaluates tasks in routes/console.php. Queue workers are long-running processes that pull and execute dispatched jobs from Redis or another driver.

Most production apps need both. The scheduler handles time-based periodic work; queue workers handle event-driven jobs that must not block HTTP responses.

Scheduled tasks fire on a clock—every minute, daily, or weekly. Queued jobs fire when dispatched by a user action, webhook, or domain event.

Reach for the scheduler when work is tied to a calendar, not a user click. If the task must run whether anyone visited the site today, schedule it. Periodic maintenance like pruning sessions, daily sales reports at 6 AM Asia/Kathmandu, subscription renewals on the 1st, supplier API syncs every four hours, SSL expiry checks, and cache warming before morning traffic all belong here. On eCommerce projects like Nepal Gift Card, marking expired gift cards at midnight is scheduler work; sending purchase confirmation at checkout is queue work. Mixing these up means customers wait up to 60 seconds for email or expired cards stay purchasable until the next cron tick.

Any work triggered by a user, webhook, or domain event that would slow the HTTP response belongs on a queue. Notifications, third-party API calls during checkout, PDF generation, Spatie Media Library image conversions, webhook delivery with retry logic, CSV bulk imports, and search index updates after model save are classic queue candidates. On Adventure Third Pole Trek, booking confirmation, supplier notification, and Khalti payment callback handling all dispatch to Redis queues so the Livewire booking form returns in under 300 ms. If removing the job makes the page feel instant, queue it.

Add exactly one crontab line under /etc/cron.d/ or the deploy user's crontab that runs php artisan schedule:run every minute against the current release path. Register tasks in routes/console.php on Laravel 12 and 13 using methods like dailyAt(), timezone(), withoutOverlapping(), and onOneServer(). Verify with php artisan schedule:list and php artisan schedule:test. On Laravel 13 with PHP 8.3+, ensure cron uses the same PHP binary as PHP-FPM. A version mismatch between cron and FPM after deploy causes cryptic class-not-found errors—the single most common scheduler failure I debug on client servers.

Install Supervisor and add a program block pointing at php artisan queue:work redis with sleep, tries, and max-time flags. Set autostart, autorestart, stopasgroup, killasgroup, numprocs for concurrency, and log to shared storage. After each Deployer 7 release, add a deploy recipe task running php artisan queue:restart. That sets a cache flag so 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 I have seen repeatedly on sister sites sharing GitLab CI pipelines.

Yes. Schedule::job(new SyncInventoryFromSupplier)->everyFourHours() lets the scheduler decide when and the queue infrastructure handle execution. This works when the same job is both user-triggered and periodically re-run. The cron entry only enqueues—the worker executes. Workers must be running or scheduled jobs pile up unprocessed. You still get $tries, backoff arrays, failed_jobs tracking, and a failed() handler that a bare scheduled command lacks for heavy or retryable work. Do not confuse scheduling a job with replacing workers entirely.

withoutOverlapping() prevents a slow scheduled task from stacking if yesterday's run is still going—a common production gotcha on under-provisioned VPS hosts. Without it, a daily report that exceeds its interval spawns overlapping copies, multiplying database load and sending duplicate emails. Pair it with onOneServer() in multi-server setups so only one app server runs the task, using a shared Redis cache driver as a mutex. Without onOneServer(), three servers each run the same renewal charge or daily report. Use both for any task whose runtime might exceed its schedule interval.

The sync driver executes queued jobs inline during the HTTP request. Fine for local development; catastrophic for checkout flows in production. PDF generation, payment verification, or confirmation email then blocks the user until SMTP or a payment gateway responds. Production should use Redis 8.10, which handles queue throughput far better than the database driver polling MySQL 9.7 and creating lock contention under load. Use the database driver only on shared hosting where Redis is unavailable, accepting the performance trade-off. Never ship sync to an app processing Khalti callbacks or order confirmations.

The scheduler orchestrates timing; the queue executes heavy or retryable units. A typical cycle on PostgreSQL 18 and Redis 8.10: at 00:05, subscriptions:renew dispatches ProcessRenewalPayment per due subscription to a payments queue; at 01:00, reports:daily dispatches GenerateReportPdf per tenant; hourly PurgeExpiredSessions runs as a scheduled job. On every booking, controllers dispatch SendBookingConfirmation, NotifySupplier, and UpdateSearchIndex to separate queues for isolation. Queue names let you assign dedicated workers so a slow PDF never blocks payment processing. Mature Laravel 12/13 apps treat both paths as complementary, not competing.

Three mistakes I see repeatedly. First, cron polling the database every minute for pending rows instead of dispatching a job when the row is created—you lose retry semantics, create race conditions across servers, and hammer the database. Second, running a 20-minute daily import inside a scheduled command; withoutOverlapping() blocks tomorrow's run and you miss a day—chunk into batched jobs instead. Third, forgetting onOneServer() so every app server runs the same task, producing duplicate reports or renewal charges. Also avoid sync queues in production and using runInBackground() for heavy work.

For the scheduler, log task output to shared storage and use emailOutputOnFailure per task on Laravel 11+. Alert on missed runs. For queues, watch queue depth, failed job count in the failed_jobs table, and worker uptime via Supervisor or Horizon. A stalled queue on a payment-heavy app means confirmed orders with no confirmation email—a support ticket avalanche. Monitor failed_jobs actively; a silent queue failure on a payment webhook is worse than a loud HTTP 500. Horizon adds a dashboard at /horizon for Redis throughput visibility—protect that route in production.

Skip the queue if the task is lightweight, idempotent, and completes in seconds—model pruning, clearing expired password reset tokens, rotating logs. Use runInBackground() to spawn a subprocess so schedule:run does not block other scheduled tasks in the same minute. Do not use runInBackground() for heavy work; spawn a queued job instead so retries and failure tracking apply. If the task needs $tries, backoff, or a failed() handler, dispatch to the queue even when triggered on a schedule. The decision is about runtime and failure handling, not just who triggers the work.

Horizon replaces manual Supervisor worker configurations with a single horizon process when you run more than two worker processes or need visibility into throughput on Redis-backed queues. It provides a dashboard at /horizon—protect it in production—plus worker balancing, memory limits, and graceful termination. At scale on Redis 8.10, Horizon handles auto-balancing better than hand-tuned numprocs in Supervisor. For simpler single-worker setups on a legal-tech portal or small booking app, raw Supervisor configs remain adequate. Install Horizon when queue depth, failed job trends, or worker count become operational concerns rather than afterthoughts.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: