Frekuensi menit ini ke menit itu hanya berguna bila task selesai sebelum interval berikutnya tiba. Laporan per jam yang ternyata butuh 70 menit akan tumpang tindih dengan pemanggilan berikutnya — dua proses berat berlari bersamaan, saling berebut database, dan menghasilkan laporan ganda. Cron klasik tidak peduli: ia hanya menyalakan mesin setiap interval tanpa bertanya apakah mesin sebelumnya sudah berhenti. Scheduler Laravel menjawabnya dengan withoutOverlapping(), yang membangun mutex berbasis atomic lock sebelum task dieksekusi. Pada chapter ini kita buktikan masalahnya lewat dua proses concurrent, lalu lihat lock-nya bekerja.
Masalahnya, Dibuktikan dengan Dua Proses #
Kita definisikan task yang menyimulasikan laporan lambat — tidur 5 detik, lalu menandai baris di sebuah berkas log:
Schedule::call(function () {
sleep(5);
file_put_contents(
storage_path('logs/overlap-demo.log'),
'eksekusi pada '.now()->format('H:i:s')."\n",
FILE_APPEND,
);
})->everyMinute()->name('laporan-lambat');Tanpa proteksi, dua proses schedule:run yang berjalan bersamaan sama-sama mengeksekusi task ini — kita jalankan proses A di background, lalu proses B satu detik kemudian:
Output yang Dihasilkan (proses A):
2026-10-13 20:22:15 Running [laporan-lambat] ....................... 5s DONEOutput yang Dihasilkan (proses B):
2026-10-13 20:22:16 Running [laporan-lambat] ....................... 5s DONEIsi overlap-demo.log memuat dua baris — task tumpang tindih:
eksekusi pada 20:22:20
eksekusi pada 20:22:21Bayangkan dua baris itu adalah dua laporan identik terkirim ke pelanggan, atau dua proses db:migrate bersaing di tabel yang sama.
Mutex Berbasis Atomic Lock #
Proteksinya bekerja lewat mekanisme lock yang atomik — hanya satu pihak yang boleh memegang kunci pada satu waktu. Sandbox memakai CACHE_STORE=database, dan kelas DatabaseStore mendukung atomic lock lewat tabel cache_locks. Perilakunya bisa kita buktikan langsung:
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\DB;
$lockA = Cache::lock('laporan-demo', 10);
$lockB = Cache::lock('laporan-demo', 10);
echo 'A_acquire: ' . var_export($lockA->acquire(), true) . "\n";
echo 'B_acquire: ' . var_export($lockB->acquire(), true) . "\n";
$lockA->release();
echo 'B_acquire_setelah_rilis: ' . var_export($lockB->acquire(), true) . "\n";
echo 'baris_cache_locks: ' . DB::table('cache_locks')->count() . "\n";
$lockB->release();
echo 'baris_cache_locks_akhir: ' . DB::table('cache_locks')->count() . "\n";Output yang Dihasilkan:
A_acquire: true
B_acquire: false
B_acquire_setelah_rilis: true
baris_cache_locks: 1
baris_cache_locks_akhir: 0Pembacaan output di atas: A berhasil memegang lock; B ditolak saat A masih memegangnya; setelah A melepas, B diterima; dan baris lock di tabel selalu bersih setelah dilepas. Method withoutOverlapping() membungkus logika persis ini — sebelum eksekusi, task mencoba mengakuisisi lock bernama framework/schedule-<hash>; gagal mengunci berarti dilewati.
withoutOverlapping dalam Aksi #
Cukup tambahkan satu method pada definisi task laporan-lambat:
Schedule::call(function () {
sleep(5);
file_put_contents(/* ... seperti di atas ... */);
})->everyMinute()->name('laporan-lambat')->withoutOverlapping(10);Skenario dua proses yang sama diulang — A di background, B menyusul satu detik kemudian:
Output yang Dihasilkan (proses A):
2026-10-13 20:22:54 Running [laporan-lambat] ....................... 5s DONEOutput yang Dihasilkan (proses B):
INFO No scheduled commands are ready to run. Isi overlap-demo.log kini hanya satu baris — eksekusi pada 20:22:59 — dan tabel cache_locks kembali nol baris setelah proses A selesai. Proses B tidak menampilkan error; task yang sedang terkunci dilewati senyap, dan baris No scheduled commands... muncul karena tidak ada task lain yang due.
Argumen 10 pada withoutOverlapping(10) adalah masa kedaluwarsa lock dalam menit. Lock hidup 600 detik sejak dibuat; angka ini penting untuk skenario terburuk — bila proses mati tanpa sempat melepas lock, task akan tetap terkunci sampai lock kedaluwarsa sendiri. Default tanpa argumen adalah 1440 menit (24 jam), jadi tugas yang crash sebelum schedule:finish berjalan akan dianggap tumpang tindih sehari penuh. Set nilai yang sedikit di atas estimasi durasi maksimum task Anda.
Satu Server Saja: onOneServer #
withoutOverlapping() berlaku pada level mesin. Di aplikasi yang berjalan di beberapa server, setiap server punya cron sendiri dan keduanya bisa memulai task yang sama pada menit yang sama. Method ->onOneServer() menyelesaikannya: scheduler mengunci per kombinasi task + menit, sehingga hanya satu server yang mengeksekusi:
Schedule::call(function () {
file_put_contents(storage_path('logs/oneserver-demo.log'), /* ... */);
})->everyMinute()->name('denormalisasi-statistik')->onOneServer();Output yang Dihasilkan (server kedua, menit yang sama):
INFO Skipping [denormalisasi-statistik] because the command already ran on another server. Server pertama mengeksekusi task dan menulis jalan pada 20:23:15 ke log; server kedua melewatkannya dengan pesan eksplisit. Syaratnya satu: cache driver harus mendukung atomic lock (database, redis, memcached, dynamodb, atau file) agar semua server berbagi sumber kebenaran yang sama.
Lock menyelesaikan masalah tumpang tindih antar-eksekusi, tapi task yang lambat tetap menyandera satu hal lain: proses schedule:run menunggu task selesai. Chapter Background Execution
membebaskan scheduler dari penantian itu. Detail mekanisme lengkap ada di dokumentasi Task Scheduling
.