Pengingat jatuh tempo invoice yang seharusnya terkirim jam 8 pagi ternyata tidak pernah sampai, dan tidak ada yang sadar sampai pelanggan mengeluh. Atau laporan harian terkirim dua kali karena prosesnya tumpang tindih. Masalah seperti ini hampir selalu berasal dari pekerjaan terjadwal yang tidak diatur dan tidak dipantau dengan baik.
Laravel Task Scheduler memindahkan semua jadwal ke dalam kode aplikasi dan hanya membutuhkan satu entri cron di server. Panduan ini membahas scheduler di Laravel 11: cara kerjanya, membuat command terjadwal, zona waktu, mencegah tumpang tindih, logging, pemasangan di cPanel dan VPS, serta cara men-debug jadwal yang tidak jalan.
1. Cara Kerja Scheduler
Cron di server memanggil php artisan schedule:run setiap menit. Perintah itu membaca semua definisi jadwal, mengecek mana yang jatuh tempo pada menit tersebut, lalu menjalankannya. Akibatnya:
- Semua jadwal tercatat di version control, bukan tersebar di crontab server.
- Developer baru cukup membaca satu file untuk tahu tugas apa saja yang berjalan.
- Jadwal bisa diuji dan dipindah ke server lain tanpa menulis ulang crontab.
| Aspek | Banyak entri cron manual | Laravel Scheduler |
|---|---|---|
| Lokasi definisi | crontab tiap server | routes/console.php |
| Cegah proses ganda | Harus buat lock sendiri | withoutOverlapping() |
| Zona waktu per tugas | Ikut zona server | timezone() per jadwal |
| Log dan notifikasi gagal | Redirect output manual | appendOutputTo(), hook onFailure() |
| Melihat daftar jadwal | crontab -l | php artisan schedule:list |
2. Membuat Command untuk Tugas Rutin
Closure memang bisa dijadwalkan, tetapi artisan command lebih mudah diuji dan dijalankan manual. Contoh pengingat invoice yang jatuh tempo:
php artisan make:command SendInvoiceReminders
namespace App\Console\Commands;
use App\Models\Invoice;
use App\Notifications\InvoiceDueSoon;
use Illuminate\Console\Command;
class SendInvoiceReminders extends Command
{
protected $signature = 'invoices:remind {--days=3 : Berapa hari sebelum jatuh tempo}';
protected $description = 'Kirim pengingat untuk invoice yang akan jatuh tempo';
public function handle(): int
{
$target = today()->addDays((int) $this->option('days'));
$count = 0;
Invoice::where('status', 'unpaid')
->whereDate('due_date', $target)
->whereNull('reminded_at')
->with('customer')
->chunkById(200, function ($invoices) use (&$count) {
foreach ($invoices as $invoice) {
$invoice->customer->notify(new InvoiceDueSoon($invoice));
$invoice->update(['reminded_at' => now()]);
$count++;
}
});
$this->info("Pengingat terkirim: {$count}");
return self::SUCCESS;
}
}
Perhatikan kolom reminded_at. Kolom ini membuat command idempoten: jika tidak sengaja berjalan dua kali, invoice yang sudah diingatkan tidak dikirimi lagi. Prinsip ini wajib untuk semua tugas terjadwal yang mengirim pesan atau mengubah uang.
3. Mendaftarkan Jadwal di routes/console.php
Di Laravel 11, app/Console/Kernel.php tidak ada lagi di proyek baru. Jadwal ditulis di routes/console.php:
use Illuminate\Support\Facades\Schedule;
Schedule::command('invoices:remind --days=3')
->dailyAt('08:00')
->timezone('Asia/Jakarta')
->withoutOverlapping()
->appendOutputTo(storage_path('logs/schedule-invoices.log'));
Schedule::command('reports:daily')
->weekdays()
->at('07:00')
->onFailure(function () {
logger()->error('reports:daily gagal dijalankan');
});
Schedule::command('model:prune')->daily();
Schedule::command('queue:prune-failed --hours=168')->weekly();
Frekuensi yang Sering Dipakai
->everyFiveMinutes();
->hourlyAt(15); // menit ke-15 setiap jam
->dailyAt('13:00');
->weekdays()->at('09:00');
->weeklyOn(1, '06:00'); // Senin 06:00
->monthlyOn(1, '00:30'); // tanggal 1 jam 00:30
->cron('0 */2 * * *'); // ekspresi cron bebas
->environments(['production']);
4. Zona Waktu
Jam di server sering diset UTC. Jika jadwal ditulis dailyAt('08:00') tanpa zona waktu dan aplikasi memakai UTC, tugas berjalan pukul 15:00 WIB. Ada dua pilihan: set APP_TIMEZONE=Asia/Jakarta di .env agar seluruh aplikasi memakai WIB, atau tambahkan ->timezone('Asia/Jakarta') per jadwal dan biarkan database tetap UTC. Pilihan kedua lebih aman jika aplikasi Anda melayani beberapa zona waktu.
5. Memasang Cron di Server
- VPS: jalankan
crontab -esebagai user yang memiliki folder proyek (misalnyawww-dataatau user deploy), lalu tambahkan baris di bawah. - cPanel: buka menu Cron Jobs, pilih "Once Per Minute", lalu isi command dengan path PHP lengkap.
- Pastikan versi PHP CLI sama dengan versi PHP web. Di cPanel, path-nya sering berbentuk
/usr/local/bin/phpatau/opt/cpanel/ea-php82/root/usr/bin/php.
* * * * * cd /home/user/aplikasi && /usr/local/bin/php artisan schedule:run >> /dev/null 2>&1
Saat development lokal, tidak perlu cron. Jalankan php artisan schedule:work yang memanggil scheduler setiap menit di terminal.
6. Mencegah Tumpang Tindih dan Tugas Lambat
withoutOverlapping(): jika eksekusi sebelumnya belum selesai, eksekusi berikutnya dilewati. Lock-nya kedaluwarsa setelah 24 jam secara default; tuliswithoutOverlapping(30)untuk 30 menit.runInBackground(): tugas lambat tidak menahan tugas lain yang dijadwalkan pada menit yang sama.onOneServer(): jika aplikasi berjalan di beberapa server, tugas hanya dijalankan sekali. Butuh cache driver bersama seperti Redis atau database.- Pindahkan pekerjaan berat ke queue: command cukup mengirim job, lalu worker yang mengerjakan.
Di shared hosting yang tidak mengizinkan proses worker permanen, Anda bisa memproses queue lewat scheduler:
Schedule::command('queue:work --stop-when-empty --max-time=50')
->everyMinute()
->withoutOverlapping();
7. Debugging Jadwal yang Tidak Jalan
- Jalankan
php artisan schedule:listuntuk melihat jadwal dan waktu eksekusi berikutnya. Jika tugas tidak muncul, kesalahan ada diroutes/console.php. - Jalankan
php artisan schedule:testuntuk mengeksekusi satu jadwal secara manual. - Jalankan command-nya langsung, misalnya
php artisan invoices:remind, untuk melihat error. - Cek apakah cron benar-benar memanggil Laravel: ganti sementara
/dev/nulldengan path file log. - Cek lock yang tertinggal. Jika proses mati di tengah jalan,
withoutOverlappingbisa menahan tugas sampai lock kedaluwarsa. Jalankanphp artisan schedule:clear-cache.
8. Kesalahan Umum
- Jadwal berjalan di jam yang salah: zona waktu server dan aplikasi berbeda.
- "Could not open input file: artisan": cron tidak masuk ke folder proyek. Tambahkan
cd /path/proyek &&. - File log tidak bisa ditulis: cron berjalan sebagai user berbeda dari web server sehingga izin folder
storagebentrok. - Email terkirim ganda: command tidak idempoten. Tambahkan penanda seperti
reminded_at.
Checklist Sebelum Produksi
- Satu entri cron per server sudah terpasang dan memakai PHP CLI yang benar.
- Setiap tugas punya zona waktu yang jelas.
- Tugas yang mengirim pesan atau mengubah data bersifat idempoten.
- Tugas panjang memakai
withoutOverlapping()atau dipindah ke queue. - Output dicatat ke log dan ada notifikasi saat gagal.
schedule:listsudah dicek setelah deploy.
Untuk tugas yang memakan waktu lama, baca queue dan job di Laravel. Langkah memasang cron di hosting juga dibahas di panduan deploy Laravel ke hosting dan deploy aplikasi Laravel ke shared hosting.