Profile    Mohammed Shiroz Status   Loading  
Logo
Share This
Back to blog
Filter by:
Tags
//Article title

Laravel Scheduling: Replace Your Messy Crontab With Code You Can Review

About Post

Somewhere there's a server with a crontab like this. Maybe you're looking at it right now.

0 2 * * * php /var/www/app/artisan reports:nightly >> /tmp/r.log
*/5 * * * * php /var/www/app/artisan payments:sync
30 7 * * 1-5 php /var/www/app/artisan rent:remind
0 3 * * * /home/deploy/cleanup.sh   # who wrote this?
# 0 4 * * * php /var/www/app/artisan old:thing  (disabled 2022?)

Nobody reviewed it. It's not in Git. Half the times are in the server's timezone, and nobody is sure which timezone that is. When you add a second server, every job runs twice. And when payments:sync takes six minutes instead of five, two copies run at the same time and fight over the same rows.

Laravel's scheduler fixes all of that, and most teams only use a fraction of it.

One cron line, forever

The idea is simple. The server gets exactly one cron entry, which runs the scheduler every minute:

* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1

Every minute, Laravel looks at your schedule definitions and runs whatever is due. The schedule itself lives in your codebase, so it goes through pull requests, has history, and deploys with the code that depends on it. Changing when a report runs becomes a reviewed one-line diff instead of an SSH session.

Where the schedule lives now

Since Laravel 11, the old app/Console/Kernel.php is gone from new apps. Schedules go in routes/console.php using the Schedule facade. Here's the crontab above, rewritten:

use Illuminate\Support\Facades\Schedule;

Schedule::command('reports:nightly')
    ->dailyAt('02:00')
    ->onOneServer()
    ->runInBackground();

Schedule::command('payments:sync')
    ->everyFiveMinutes()
    ->withoutOverlapping(10);

Schedule::command('rent:remind')
    ->weekdays()
    ->at('07:30')
    ->timezone('Asia/Qatar');

Schedule::command('model:prune')->daily();

It reads like a sentence. A new developer can open one file and know every recurring thing the app does. If you prefer, you can also define the schedule in bootstrap/app.php with ->withSchedule(...), but one dedicated file is easier to find.

The four methods that prevent real incidents

withoutOverlapping(): no two copies at once

If a run is still going when the next one is due, the new one is skipped. Laravel does this with a cache lock. The lock expires after 24 hours by default, which matters: if the server dies mid-run, the lock stays until it expires and the job silently skips. Pass a sensible number of minutes, like withoutOverlapping(10), for frequent jobs.

onOneServer(): one run across the whole fleet

Two web servers both running the cron means every task runs twice. Reminders get sent twice. Reports get generated twice. onOneServer() makes the first server to grab the lock run the task and the others skip it. It needs a shared cache that all servers talk to (Redis, Memcached, DynamoDB or the database driver), because a lock in each server's local file cache protects nothing.

runInBackground(): don't block the queue of tasks

Tasks due at the same minute run one after another by default. A slow nightly report can delay everything scheduled after it. runInBackground() starts it as a separate process so the rest of the schedule carries on. It works for command and exec tasks.

timezone(): say what you mean

"Send rent reminders at 7:30" means 7:30 for the tenant, not for a server that happens to be in UTC. Set the timezone per task, or set schedule_timezone in config/app.php for all of them. In countries with daylight saving, be careful with tasks between 1 and 3 AM: on change days they can run twice or not at all.

The default I use: anything that sends messages or touches money gets withoutOverlapping() and onOneServer(), even if you only have one server today. Tomorrow you'll add a second one and forget.

Heavy work belongs on the queue

The scheduler decides when. It shouldn't do the heavy lifting itself. For anything slow, schedule a job and let your queue workers handle retries and timeouts:

Schedule::job(new GenerateMonthlyStatements)
    ->monthlyOn(1, '06:00')
    ->onOneServer();

One small trap: if you schedule a closure with Schedule::call() and want onOneServer(), give it a name with ->name('cleanup-temp-files'). Laravel needs that name to build the lock key.

A schedule that fails silently is worse than none

Cron's worst habit is failing without telling anyone. The nightly report stopped three weeks ago and you find out when someone asks where it went. Add visibility:

Schedule::command('reports:nightly')
    ->dailyAt('02:00')
    ->onFailure(function () {
        Log::error('Nightly report failed');
    })
    ->pingOnSuccess(config('services.heartbeat.nightly_report'));

The failure hook catches tasks that run and fail. The success ping catches the scarier case: the task that never ran at all, because the cron line vanished or the server was rebuilt. Point it at any heartbeat monitoring service that alerts you when an expected ping doesn't arrive.

Tools you'll use every week

  • php artisan schedule:list shows every task and when it runs next. Great for reviewing a pull request.
  • php artisan schedule:test lets you pick a task and run it now.
  • php artisan schedule:work runs the scheduler in the foreground locally, no cron needed.
  • ->environments(['production']) keeps a task from running on staging, where it might message real people.

Migration checklist

  1. Copy every line from the old crontab into routes/console.php, including the strange shell scripts (wrap them with Schedule::exec() or turn them into commands).
  2. Decide the timezone for each one explicitly.
  3. Add overlap and one-server protection where duplicates would hurt.
  4. Add failure hooks and heartbeats for anything people depend on.
  5. Replace the crontab with the single schedule:run line, then check schedule:list on the server.

The full list of frequency options and hooks is in the Laravel scheduling docs.

What's the strangest thing you've found living in a server's crontab? I'm fairly sure everyone has at least one "who wrote this?" line.

Comments (0)
Leave your review

Thanks for your valuable comments. Your comments has been updated and appreciate your getting in touch...

01. About Shiroz

Mohammed Shiroz

Hi, I'm Mohammed Shiroz, a software engineer and AI enthusiast from Sri Lanka who turns ideas into intelligent, real-world solutions. With over 9 years of hands-on experience, I currently lead real estate ERP development at Kate Group, a...

03.My Projects

04. Categories

Ready To order Your Project ?

Get in Touch
Close