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.

Master cron for Scheduled Jobs

By Kokil Thapa | Last reviewed: September 2026

When you need to master cron for scheduled jobs, you are really learning how Linux turns a clock into a reliable worker. Cron fires commands at fixed intervals without a human clicking anything. That sounds simple until a backup never runs, a Laravel invoice job fires twice, or a path breaks after deployment. On production Linux servers in Nepal and abroad, cron is still the default trigger for nightly dumps, cache clears, report emails, and framework schedulers. This guide walks through crontab syntax, Laravel integration, monitoring, and the mistakes I see on real client projects.

What is cron and how does it schedule jobs on Linux?

Cron is the time-based job scheduler built into Unix-like systems. The cron daemon reads crontab files and launches shell commands when the current minute matches each entry. Each line is independent. There is no central queue unless your application provides one.

On Ubuntu 22.04 and 24.04 servers I maintain, cron runs as a system service. User crontabs live per account. System-wide jobs sit in /etc/cron.d/, /etc/cron.daily/, and related directories. For application work, I almost always use the deploy user's crontab rather than root unless the task truly needs elevated privileges.

How Cron Schedules JobsSystem ClockEvery minute tickCron DaemonReads crontabCrontab FileUser or systemMatch minute, hour, day, month, weekdayIf all fields match, run the commandShell CommandScript or binaryLog Outputsyslog or file
Cron daemon flow: clock tick, crontab match, command execution, and logging for scheduled jobs

Where crontab entries live

User crontabs are edited with crontab -e and stored under /var/spool/cron/crontabs/. List them with crontab -l. System jobs in /etc/cron.d/ include a user column. That format matters when PHP-FPM runs as www-data but your app deploys as deploy.

For a quick orientation, see the companion piece on how cron jobs schedule tasks on Linux. It covers the same fundamentals with fewer framework specifics.

How do you write a correct crontab schedule expression?

A standard crontab line has five time fields plus a command. Order is minute, hour, day of month, month, day of week. Values range from 0–59 for minutes and 0–23 for hours. Day of week uses 0 or 7 for Sunday on most systems.

# m h dom mon dow command
0 2 * * * /usr/local/bin/backup.sh
*/15 * * * * /usr/bin/php /var/www/app/artisan queue:work --stop-when-empty
0 0 1 * * /usr/bin/find /var/log/app -name "*.log" -mtime +30 -delete

The first example runs daily at 02:00. The second runs every fifteen minutes. The third runs at midnight on the first day of each month. Special strings like @daily and @reboot also work, though I prefer explicit numeric fields in production because they are easier to audit.

Crontab field reference

FieldAllowed valuesExampleMeaning
Minute0–59*/5Every 5 minutes
Hour0–232-4Hours 2, 3, and 4
Day of month1–311,151st and 15th
Month1–12*Every month
Day of week0–71-5Monday through Friday

Use a regex tester mindset when validating patterns. Cron is not regex, but the habit of testing before production saves hours. The official crontab manual page remains the authoritative reference for field semantics.

A common mistake is mixing day-of-month and day-of-week with unintended OR logic. If both are restricted, cron runs when either condition matches on many implementations. When you need AND logic, wrap logic inside a shell script instead.

How do you run Laravel scheduled tasks with cron in production?

Laravel does not rely on dozens of crontab lines. You register tasks in routes/console.php or app/Console/Kernel.php on older versions. One system cron entry calls php artisan schedule:run every minute. Laravel decides which defined tasks fire in that minute.

On Laravel 13.x with PHP 8.3 or higher, a typical production entry looks like this:

* * * * * cd /var/www/myapp/current && /usr/bin/php8.3 artisan schedule:run >> /var/log/myapp-scheduler.log 2>&1

Notice three details. The path uses cd so relative paths resolve correctly. The PHP binary is explicit because servers often carry multiple versions side by side. Output redirects to a log file because cron emails are easy to miss.

I use this pattern on booking platforms like Adventure Third Pole Trek where reminders, supplier sync, and report generation must run on time. The application code lives in version control. Cron only triggers the scheduler.

Define tasks in Laravel

use Illuminate\Support\Facades\Schedule;

Schedule::command('reports:daily')
    ->dailyAt('06:30')
    ->timezone('Asia/Kathmandu')
    ->withoutOverlapping()
    ->appendOutputTo(storage_path('logs/daily-report.log'));

Schedule::job(new SendPaymentReminders)
    ->hourly()
    ->onOneServer();

The withoutOverlapping() mutex prevents a slow run from stacking duplicates. The onOneServer() call requires a shared cache like Redis 8.10 when multiple app servers exist. For deeper Laravel scheduling setup, read Laravel scheduled tasks in production.

Laravel Scheduler + CronSystem CronEvery minuteschedule:runArtisan commandTask Registryconsole.phpArtisan Commandreports:dailyQueued JobSendRemindersClosure TaskInline logicLogs, DB, email, queue dispatchBusiness outcome
One cron line runs Laravel schedule:run each minute; the framework dispatches registered commands, jobs, and closures

WordPress 7.1 and WooCommerce 11.1 use a different model. WP-Cron triggers on page visits unless disabled. On production sites I disable pseudo-cron and use system cron to call wp-cron.php instead. Magento 2.4.x relies on its own cron consumer groups. The principle stays the same: one reliable system trigger beats hope.

How do cron jobs differ from queue workers for background processing?

Cron answers the question "run this at 03:00." A queue worker answers "process this job when a worker is free." They overlap in Laravel because scheduled tasks can dispatch jobs. They are not interchangeable.

Use cron when timing matters. Examples include nightly backups, monthly billing, certificate renewal checks, and sitemap generation. Use queues when volume and retries matter. Examples include sending 2,000 emails, resizing uploads, and calling slow third-party APIs.

CriteriaCron / SchedulerQueue Worker
TriggerWall-clock scheduleJob pushed to queue
ConcurrencyOne run per schedule slotMany workers in parallel
RetriesManual or app-levelBuilt-in with backoff
Long tasksRisk overlap if slowDesigned for long work
Best forReports, cleanup, sync windowsEmail bursts, imports, webhooks

The dedicated comparison at Laravel cron vs queue worker walks through decision rules. For high-traffic apps, pair a single scheduler cron with persistent queue:work systemd units or Supervisor processes. Read scaling Laravel queues for high traffic for worker sizing guidance.

On a legal-tech portal I built, document expiry reminders run on a schedule. The scheduler dispatches individual notification jobs to the queue. Cron stays fast. Heavy work happens asynchronously. That split is the pattern I recommend most often.

How do you debug and monitor cron jobs that fail silently?

Cron failures are quiet by default. If a command produces no output and exits zero, you see nothing. If it fails before logging starts, you may still see nothing. Treat observability as part of the job definition, not an afterthought.

Verification checklist

  1. Confirm the crontab owner matches the deploy user with crontab -l -u deploy.
  2. Check syslog with grep CRON /var/log/syslog on Ubuntu.
  3. Read your redirect log file after the expected run time.
  4. Run the exact command manually from an SSH session as the same user.
  5. Verify environment variables; cron runs with a minimal PATH.
  6. Confirm Deployer symlink paths if you use zero-downtime releases.

I have lost hours to stale paths in cron after a Deployer 7 release swap. The fix is always the same: point cron at /var/www/app/current, not a dated release folder. Sister sites on my shared pipeline taught me to document the cron user in the deploy README.

# Good: stable symlink path
* * * * * cd /var/www/notary/current && /usr/bin/php artisan schedule:run >> storage/logs/cron.log 2>&1

# Bad: hard-coded release path that breaks on next deploy
* * * * * /usr/bin/php /var/www/notary/releases/47/artisan schedule:run
Debug Silent Cron FailuresJob did not run?Check sysloggrep CRONWrong user?crontab -l -uBad PATH?Use full pathsRun manuallySame shell userFix and logRedirect outputAdd health check or alert on log age
Decision flow for diagnosing cron jobs that fail silently on production Linux servers

For backup jobs, follow the rsync and cron guide at automate server backups with rsync and cron. Add a simple alert if the log file has not been updated in 26 hours. Uptime monitors and log watchers both work. The goal is to learn about failure before a client does.

Laravel provides schedule:monitor on supported versions to track expected run times. External uptime tools can HTTP ping a signed route that confirms last successful batch completion. Pick one layer of proof minimum per critical job.

What are common cron mistakes on production servers?

Most cron incidents I troubleshoot are environmental, not syntax errors. The schedule expression is correct. Everything else is wrong.

  • Relative paths: Cron starts in the user's home directory. Always cd to the app root first.
  • Missing PHP binary: Specify /usr/bin/php8.3 when multiple versions coexist.
  • Permission drift: Files owned by root after a manual fix block the deploy user.
  • Duplicate schedulers: Two crontab lines on blue-green servers without onOneServer() cause double billing.
  • Timezone blindness: Server UTC vs app Asia/Kathmandu shifts reports by five hours and fifteen minutes.
  • No overlap guard: A slow import run twice stacks until the database locks.

Nepal-based businesses often need Bikram Sambat dates in reports. Keep calendar conversion in application code. Use a Nepali date converter for validation during development. Do not ask cron to understand BS dates directly.

For WordPress stacks under WordPress development, disable WP-Cron in wp-config.php and hit wp cron event run --due-now from system cron. For custom PHP on Symfony 8.1, Symfony Scheduler can replace raw crontab entries with PHP-defined schedules similar to Laravel.

Production Cron: Before vs AfterBefore (fragile)php artisan schedule:runRelative path, no logHard-coded release folderAfter (stable)cd current && php8.3 artisanLog redirect, mutex lockMonitor log freshnessOutcomes after fixing cron setupBackups run nightly — reports arrive on timeNo duplicate charges — faster incident responseDeploys no longer break scheduled jobs
Before-and-after view of production cron hardening for scheduled jobs on Deployer-managed Laravel apps

Security matters too. Cron runs with the privileges of its owner. Do not store secrets in crontab lines where crontab -l exposes them. Load secrets from .env inside the application. Restrict file permissions on log directories.

If cron supports infrastructure you do not want to own, consider ongoing support and maintenance. A missed backup cron line costs far more than Rs 5,000/month (~USD 37) in monitoring time.

Platform-specific notes for 2026 stacks

On Laravel 12 supported through February 2027, scheduling works the same way as Laravel 13.x. Plan PHP 8.3 before upgrading to Laravel 13. MySQL 9.7 backup jobs should use mysqldump with credentials from a secured option file, not inline passwords.

Redis-backed queues complement cron but do not replace it. Memcached 1.6.x caches session data; it does not schedule work. Node.js 26 LTS build scripts belong in CI, not cron, unless you truly need a nightly asset rebuild on the server.

For broader Ubuntu-specific examples, see the Ubuntu cron jobs guide and background jobs vs cron design choice. The Laravel scheduling documentation covers mutex, timezone, and maintenance mode behaviour.

Key Takeaways

  • One well-formed crontab line with absolute paths and log redirection beats five undocumented entries.
  • Laravel needs only * * * * * schedule:run; define timing inside the app with overlap guards.
  • Use cron for clock-driven work and queues for volume-driven work; combine them when schedules dispatch jobs.
  • After every Deployer release, confirm cron still points at the current symlink, not an old release path.
  • Monitor log freshness or use schedule:monitor so silent failures surface before customers notice.
  • Set explicit timezones in application schedulers when server UTC and business local time differ.

People Also Ask

How often should cron run for Laravel scheduled tasks?

Run php artisan schedule:run every minute. Laravel evaluates which registered tasks are due during that minute. Running less often skips sub-hourly schedules unless you redesign them.

Can two cron jobs run at the same time?

Yes. Cron starts independent processes. If that is unsafe for your task, use flock, Laravel's withoutOverlapping(), or a dedicated queue worker so only one instance processes the work.

Why does my cron job work in SSH but not in crontab?

SSH loads your shell profile and PATH. Cron does not. Use full binary paths, set cd to the project root, and export required variables inside the command or a wrapper script.

Should I use root crontab for web application tasks?

Usually no. Run jobs as the deploy or application user so file permissions stay consistent. Use root only for system tasks like certificate renewal or log rotation that truly require it.

Build reliable scheduled jobs on your stack

To master cron for scheduled jobs, treat the crontab line as production code. Review it during deploys, log every run, and test as the cron user before closing the ticket. Pair system cron with framework schedulers on Laravel, disable pseudo-cron on WordPress, and keep queue workers separate for heavy lifting. If your server still misses backups or sends duplicate invoices, the fix is usually path, user, or timezone — not the schedule syntax itself.

Need help hardening cron on a live app, Laravel scheduler, or backup pipeline? See custom software development and Notary Nepal for examples of scheduled workflows in production. Browse more guides on the blog, explore free developer tools, or contact us to audit your current cron setup.

Frequently Asked Questions

Cron is the time-based job scheduler built into Unix-like systems. The cron daemon reads crontab files and launches shell commands when the current minute matches each entry. Each line is independent, with no central queue unless your application provides one. On Ubuntu 22.04 and 24.04 servers I maintain, cron runs as a system service. User crontabs are edited per account with crontab -e. For application work, use the deploy user's crontab rather than root unless the task truly needs elevated privileges.

A standard crontab line has five time fields plus a command: minute, hour, day of month, month, and day of week. Minutes run 0–59, hours 0–23, and day of week uses 0 or 7 for Sunday on most systems. Special strings like @daily work, though explicit numeric fields are easier to audit in production. A common mistake is mixing day-of-month and day-of-week with unintended OR logic on many implementations. When you need AND logic, wrap the check inside a shell script instead of relying on crontab fields alone.

Run php artisan schedule:run every minute. Laravel evaluates which registered tasks are due during that minute. Running less often skips sub-hourly schedules unless you redesign them inside the application.

Register tasks in routes/console.php, then add one crontab line calling schedule:run every minute. On Laravel 13.x with PHP 8.3, a typical entry uses cd to the app root, an explicit PHP binary like /usr/bin/php8.3, and log redirection because cron emails are easy to miss. Define timing in code with Schedule::command(), set timezone('Asia/Kathmandu') when business time differs from server UTC, use withoutOverlapping() to block slow duplicate runs, and onOneServer() with Redis 8.10 when multiple app servers share the workload.

Cron answers when to run something on a wall-clock schedule. Queue workers answer how to process jobs when a worker is free. Use cron for nightly backups, monthly billing, certificate checks, and sitemap generation where timing matters. Use queues for high-volume work like bulk email, image resizing, and slow API calls where retries and parallel workers matter. In Laravel, scheduled tasks can dispatch queue jobs, which keeps the cron trigger fast while heavy work runs asynchronously. Pair one scheduler cron with persistent queue:work processes on busy apps.

Cron failures are quiet by default, so treat observability as part of the job definition. Confirm the crontab owner with crontab -l -u deploy, check syslog via grep CRON /var/log/syslog on Ubuntu, read your redirect log after the expected run time, and run the exact command manually as the same user. Verify PATH differences, environment variables, and Deployer symlink paths. For backups, alert if the log file has not updated in 26 hours. Laravel schedule:monitor or an HTTP ping to a signed route confirming last batch completion adds proof beyond silence.

SSH loads your shell profile and PATH; cron does not. Use full binary paths, cd to the project root first, and export required variables inside the command or a wrapper script.

Usually no. Run jobs as the deploy or application user so file permissions stay consistent. Use root only for system tasks like certificate renewal or log rotation that truly require it.

Yes. Cron starts independent processes, so overlapping schedules both execute unless you prevent it. On multi-server setups, two crontab lines calling schedule:run without onOneServer() can double-fire billing or reminder jobs. Use flock for shell scripts, Laravel's withoutOverlapping() mutex for slow scheduled commands, or route heavy work to queue workers when controlled concurrency and retries matter more than a fixed clock trigger.

Most incidents I troubleshoot are environmental, not syntax errors. Relative paths fail because cron starts in the user's home directory, so always cd first. Missing explicit PHP binaries break when multiple versions coexist side by side. Permission drift after manual root fixes blocks the deploy user. Duplicate schedulers on blue-green servers cause double runs. Server UTC versus app Asia/Kathmandu shifts reports by five hours and fifteen minutes. Slow imports without overlap guards stack until databases lock. After Deployer 7 releases, stale paths pointing at old release folders instead of current are a repeat offender.

WordPress and WooCommerce use WP-Cron, which triggers on page visits unless disabled. On production sites I disable pseudo-cron in wp-config.php and use system cron to call wp-cron.php or run wp cron event run --due-now on a fixed interval. That gives the same reliability principle as Laravel: one system trigger you control, with absolute paths and logging, instead of hoping traffic arrives on time. Heavy plugin tasks still benefit from separating quick cron triggers from longer background processing where your stack supports it.

Point cron at the stable current symlink, not a dated release folder. A good Laravel entry looks like cd /var/www/myapp/current followed by the PHP binary and artisan schedule:run with output redirected to a log file. A bad entry hard-codes /var/www/myapp/releases/47/, which breaks on the next Deployer 7 swap. I document the cron user in the deploy README on shared pipelines because this mistake wastes hours and silently stops backups, reports, and scheduler runs until someone notices missing log output.

Production servers often run UTC while the business operates on Asia/Kathmandu. Set explicit timezones in Laravel schedulers with timezone('Asia/Kathmandu') rather than assuming the server clock matches local business hours. A UTC server can shift daily reports by five hours and fifteen minutes if you ignore this. For Bikram Sambat dates in reports, keep calendar conversion in application code and validate with a Nepali date converter during development. Do not expect cron itself to understand BS dates.

No. Cron runs with the privileges of its owner, and crontab -l exposes inline secrets to anyone with shell access on that account. Load credentials from .env inside the application instead. For MySQL 9.7 backup jobs, use mysqldump with credentials from a secured option file rather than inline passwords in the crontab command. Restrict file permissions on log directories that capture command output, since redirected logs can leak connection strings or paths if you are careless with verbosity.

A missed backup cron line costs far more than Rs 5,000/month (~USD 37) in monitoring time alone, before data loss or duplicate billing from silent scheduler failures. Add a simple alert when a critical log file has not updated within 26 hours. Uptime monitors, log watchers, Laravel schedule:monitor, or an external HTTP ping to a signed route that confirms last successful batch completion all work. The goal is learning about failure before a client notices missing reports, stale backups, or invoices sent twice.

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: