Di lokal, tugas terjadwal kita uji dengan menjalankan php artisan schedule:run secara manual — di production tidak ada orang yang rela mengetik perintah itu setiap menit. Yang dibutuhkan server adalah mekanisme yang memanggil scheduler setiap menit, selamanya. Kabar baiknya: seluruh jadwal aplikasi cukup ditopang satu baris cron, bukan satu baris per task seperti crontab klasik. Chapter ini membahas pemasangan baris tersebut, timezone yang memengaruhi jam eksekusi, onOneServer untuk klaster multi-server, dan cara memastikan scheduler benar-benar hidup.

Satu Baris cron untuk Seluruh Jadwal #

Satu-satunya konfigurasi level server yang dibutuhkan scheduler Laravel adalah entri crontab berikut (contoh tampilan untuk server produksi Toko Budi):

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

Empat bagian menyusun baris ini: lima bintang * * * * * berarti “setiap menit”; cd /var/www/tokobudi && memastikan artisan berjalan dari direktori aplikasi karena .env dan artisan dibaca relatif terhadap direktori kerja; dan >> /dev/null 2>&1 membuang output karena kegagalan task sudah dicatat scheduler ke laravel.log — email cron per menit hanya menambah kebisingan. Inilah pola yang dianjurkan dokumentasi deployment : satu entri, seluruh jadwal.

Desain satu-entri ini bekerja karena schedule:run bukan pelaksana task, melainkan hakim: setiap terpanggil, perintah ini memeriksa seluruh definisi jadwal dan memilih mana yang jatuh tempo pada menit ini. Kita buktikan dengan tiga task produksi di routes/console.php:

php
<?php

use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\Schedule;

Artisan::command('laporan:ringkas', function () {
    $this->info('Ringkasan penjualan selesai dibuat.');
})->purpose('Buat ringkasan penjualan harian');

Artisan::command('dashboard:bersihkan', function () {
    $this->info('Cache dashboard dibersihkan.');
})->purpose('Bersihkan cache statistik dashboard');

Artisan::command('backup:basisdata', function () {
    $this->info('Backup database selesai.');
})->purpose('Backup database produksi');

Schedule::command('laporan:ringkas')->dailyAt('06:00')->name('laporan-harian');

Schedule::command('dashboard:bersihkan')->everyFiveMinutes()->name('bersihkan-dashboard');

Schedule::command('backup:basisdata')->dailyAt('03:00')->name('backup-basisdata')->onOneServer();

Inventarisnya terlihat lewat schedule:list:

bash
php artisan schedule:list

Output yang Dihasilkan:

text
  0   6 * * *  php artisan laporan:ringkas ........ Next Due: 7 hours from now
  */5 * * * *  php artisan dashboard:bersihkan . Next Due: 40 seconds from now
  0   3 * * *  php artisan backup:basisdata ....... Next Due: 4 hours from now

Sedangkan pemanggilan pada menit tanpa jadwal jatuh tempo hanya menjawab:

bash
php artisan schedule:run

Output yang Dihasilkan:

text
   INFO  No scheduled commands are ready to run.  

Menit seperti ini adalah mayoritas hidup cron entry — bangun, memeriksa, pulang; baris Running [...] DONE baru muncul saat jadwal jatuh tempo. Granularitas tertingginya satu menit, mengikuti frekuensi cron.

Server Tanpa cron: schedule:work #

Sebagian lingkungan hosting tidak memberi akses crontab sama sekali. Untuk kasus ini Laravel menyediakan schedule:work (contoh tampilan, dijalankan sebagai proses panjang):

bash
php artisan schedule:work

Perintah ini memanggil schedule:run setiap menit di dalam satu proses PHP yang terus hidup — perilaku daemon yang persis seperti queue:work pada chapter Queue Worker di Production , termasuk konsekuensinya: bila prosesnya mati, tidak ada yang membangkitkannya kembali. Karena itu pola cron tetap lebih tahan banting; schedule:work paling pas untuk local development atau container yang mengawasi prosesnya sendiri.

Timezone Server dan Aplikasi #

Dua jam bekerja di belakang layar: jam server yang dipakai cron daemon memicu entri, dan jam aplikasi yang dipakai scheduler menilai jatuh tempo. Kita periksa keduanya di sandbox:

bash
php artisan config:show app.timezone

Output yang Dihasilkan:

text
  app.timezone ........................................................... UTC  
bash
date

Output yang Dihasilkan:

text
Tue Oct 13 22:53:28 UTC 2026

Cron memicu schedule:run menurut jam server, tetapi yang menentukan jatuh tempo adalah app.timezone: dailyAt('06:00') berarti pukul 06:00 menurut zona aplikasi. Bila aplikasi berjalan dengan UTC sementara pengguna kita ada di Jakarta, laporan “pagi” justru terkirim pukul 13:00 WIB. Dua jalan keluarnya: samakan app.timezone dengan zona pengguna di .env (APP_TIMEZONE=Asia/Jakarta), atau konsisten dengan UTC dan geser jam jadwal secara sadar. Per task pun tersedia ->timezone('Asia/Jakarta') bila hanya satu jadwal yang butuh zona berbeda, selengkapnya di dokumentasi Task Scheduling .

Banyak Server: onOneServer #

Ketika aplikasi berjalan di lebih dari satu server dan crontab terpasang di semuanya, setiap server memicu schedule:run pada menit yang sama — laporan harian bisa terkirim dua kali. Metode onOneServer() (terpasang pada task backup-basisdata di atas) mengatasinya lewat mutex di cache. Kita buktikan dengan dua schedule:run berturut-turut pada menit yang sama — mensimulasikan dua server:

bash
php artisan schedule:run

Output yang Dihasilkan (server pertama):

text
  2026-10-13 22:49:53 Running ['artisan' laporan:ringkas] ...... 209.26ms DONE
  ⇂ ringkasan-penjualan  

Output yang Dihasilkan (server kedua, menit yang sama):

text
   INFO  Skipping [ringkasan-penjualan] because the command already ran on another server.  

Server kedua pasrah tanpa mengeksekusi apa pun; jejak keputusannya tersimpan sebagai baris lock di tabel cache_locks:

text
laravel-cache-framework/schedule-52035b90d0502d5ede59b94db7ad14b9bc563e182248 (owner: mcRCkkQXFKbdAbdg..., exp: 23:48:17)
laravel-cache-framework/schedule-52035b90d0502d5ede59b94db7ad14b9bc563e182249 (owner: R7Xokc0Deg3M0yOG..., exp: 23:49:53)

Akhiran 2248 dan 2249 pada nama key adalah jam-menit (HHMM) pembuatan lock, dengan masa berlaku satu jam — server yang lebih dulu menulis lock memenangkan menit itu.

User cron dan Kesehatan Scheduler #

Cron entry sebaiknya dijalankan sebagai user yang sama dengan web server (misalnya www-data), dipasang lewat crontab -u www-data -e di server. Alasannya: scheduler menulis ke storage/logs dan cache, sehingga user cron harus punya izin tulis pada direktori yang sama dengan PHP-FPM — anatomi izinnya sudah kita bahas di Storage Permissions . Cron sebagai root hanya meninggalkan berkas yang tak bisa ditulis ulang web server.

Terakhir, pastikan scheduler benar-benar hidup. Pemeriksaan tercepat adalah SSH ke server dan menjalankan schedule:list. Pemeriksaan yang lebih dalam adalah heartbeat — sebuah task everyMinute() yang menuliskan stempel waktu ke cache lalu dipantau dari luar — pola lengkapnya beserta command schedule:heartbeat-status sudah kita bangun di Schedule Monitoring .

Scheduler kini berdetak sendiri di produksi. Tumpukan yang menyertai tiap rilis berikutnya adalah rangkaian cache — config, route, dan view yang dibangun saat deployment — dibahas di Caching di Production .