Laravel Scheduler zamienia dziesiątki wpisów w cronie na jeden — zadania cykliczne definiujesz w kodzie PHP, wersjonujesz w repozytorium i widzisz w jednym miejscu (php artisan schedule:list). Ten poradnik pokazuje konfigurację od podstaw, wszystkie istotne metody (withoutOverlapping, onOneServer, runInBackground) i najczęstszą pułapkę: zadania schedulera, które powinny trafiać do kolejki, a wykonują się synchronicznie i blokują cron.
Najważniejsze: Najważniejsze — Scheduler uruchamia się jednym wpisem w cronie:
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1. Właściwe zadania definiujesz wroutes/console.php(Laravel 11+) alboapp/Console/Kernel.php. Zasada produkcyjna: scheduler ma kolejkować pracę (Schedule::job()alborunInBackground()), nie wykonywać jej synchronicznie. Każde zadanie cykliczne zabezpieczajwithoutOverlapping().
Jak działa Laravel Scheduler
Klasyczny cron wymaga wpisu per zadanie — pięć zadań to pięć linii w crontabie, rozsianych po serwerach i niespójnych między środowiskami. Laravel odwraca model: w cronie jest jeden wpis, który co minutę uruchamia schedule:run. Laravel sprawdza, które zadania mają się w tej minucie wykonać, i wykonuje je.
# crontab -e — jedyny wpis, jaki potrzebujesz
* * * * * cd /var/www/app && php artisan schedule:run >> /dev/null 2>&1
Zadania definiujesz w kodzie — w Laravelu 11+ w routes/console.php
use Illuminate\Support\Facades\Schedule;
Schedule::command('reports:daily')->dailyAt('06:00');
Schedule::command('invoices:sync')->everyFifteenMinutes();
Schedule::job(new SyncPrices)->hourly();
Schedule::call(fn () => Cache::forget('stats'))->daily();
- Zalety tego modelu: definicje są wersjonowane razem z kodem, środowiska nie rozjeżdżają się, a podgląd całości to jedno polecenie
php artisan schedule:list. W development można też uruchomićphp artisan schedule:work— proces, który sam czeka na właściwą minutę, bez dotykania crona.
Scheduler + kolejki — wzorzec produkcyjny
Najważniejsza decyzja projektowa: co scheduler robi, a czego nie robi. Scheduler ma tylko zdecydować „teraz" i przekazać pracę do kolejki. Wykonywanie ciężkiej pracy bezpośrednio w schedulerze blokuje kolejne zadania crona — schedule:run czeka na zakończenie zadania, a kolejne minuty mijają.
// ŹLE — 5-minutowy raport blokuje cron na 5 minut
Schedule::command('reports:generate')->hourly();
// DOBRZE — komenda sama w sobie szybka, ciężka praca w kolejce
Schedule::job(new GenerateDailyReport)->hourly();
// DOBRZE — komenda uruchomiona w tle, cron nie czeka
Schedule::command('reports:generate')->hourly()->runInBackground();
Trzy warianty przekazania pracy do tła
-
Schedule::job(new GenerateDailyReport)— zadanie trafia wprost do kolejki; najczystszy wariant dla nowego kodu. -
Schedule::command('reports:generate')->runInBackground()— komenda startuje jako osobny proces; dobra, gdy komenda już istnieje i działa długo. -
- Wewnątrz komendy:
GenerateDailyReport::dispatch()— gdy komenda robi kilka rzeczy, z których tylko część jest ciężka.
Warunek działania wszystkich trzech: sprawne workery pod Supervisorem (albo Horizon). Scheduler bez działających workerów to kolejka, która rośnie w nieskończoność — dlatego oba elementy konfigurujesz razem. Konfigurację workerów opisuję w poradniku Supervisor i systemd dla kolejek Laravel.
Częstotliwości i ograniczenia — ściągawka
Schedule::command('sync:orders')->everyFiveMinutes();
Schedule::command('reports:daily')->dailyAt('06:00');
Schedule::command('reports:weekly')->weeklyOn(1, '07:00'); // poniedziałek 7:00
Schedule::command('cleanup:temp')->monthlyOn(1, '03:00');
Schedule::job(new SyncPrices)->weekdays()->at('08:00')->timezone('Europe/Warsaw');
Metody, które ratują produkcję
| Metoda | Co robi | Kiedy obowiązkowa |
|---|---|---|
withoutOverlapping() |
Blokuje równoległe uruchomienie tego samego zadania | Zawsze, gdy zadanie może trwać dłużej niż interwał |
onOneServer() |
Tylko jeden serwer z klastra wykona zadanie | Przy wielu serwerach aplikacji (wymaga wspólnego cache) |
runInBackground() |
Cron nie czeka na zakończenie zadania | Długie komendy |
timezone('Europe/Warsaw') |
Strefa czasowa zadania | Gdy serwer działa w UTC, a godziny mają znaczenie biznesowe |
evenInMaintenanceMode() |
Zadanie działa też w trybie maintenance | Synchronizacje, które nie mogą czekać |
emailOutputTo() |
Wysyła output zadania mailem | Rzadkie raporty i joby rozliczeniowe |
Przykład z pełnym zabezpieczeniem
Schedule::job(new ImportProductsFromErp)
->everyThirtyMinutes()
->withoutOverlapping(10) // blokada wygasa po 10 minutach
->onOneServer()
->timezone('Europe/Warsaw');
Uwaga:
withoutOverlapping()bez argumentu trzyma blokadę do zakończenia zadania. Jeśli worker padnie w trakcie (np. OOM), blokada zostaje na zawsze i zadanie nigdy więcej się nie uruchomi. Podaj limit minut (withoutOverlapping(10)) jako bezpiecznik.
withoutOverlapping pod spodem — jak to działa
Blokada to wpis w cache (cache lock) o kluczu zadania. Pierwszy proces zakłada blokadę, kolejne ją widzą i pomijają uruchomienie. Po zakończeniu zadania blokada jest zwalniana. Konsekwencje:
- Blokada wymaga współdzielonego cache — przy wielu serwerach
filecache nie zadziała; potrzebny Redis/database. - Zablokowane uruchomienie nie jest kolejkowe — zadanie po prostu nie startuje w tej minucie.
- Jeśli widzisz w logach „Skipping [zadanie] because it's already running" mimo że nic nie działa — blokada zawisła; usuń ją (
cache:cleardla klucza albo dodaj limit minut).
Podgląd, testowanie i debug schedulera
php artisan schedule:list # co, kiedy i jak często się uruchomi
php artisan schedule:run # ręczne uruchomienie tego, co wypada „teraz"
php artisan schedule:work # lokalny demon (zamiast crona) w development
php artisan schedule:test # interaktywny wybór zadania do testu
Do monitoringu produkcji: każde zadanie schedulera warto logować (->onSuccess(), ->onFailure() z Laravela 10+)
Schedule::command('invoices:sync')
->everyFifteenMinutes()
->onSuccess(fn () => Log::info('invoices:sync OK'))
->onFailure(fn () => Log::error('invoices:sync FAILED'));
Alternatywnie emailOutputTo() dla zadań, których wynik chcesz widzieć na mailu (np. nocne raporty). A jeśli chcesz pełny obraz — Laravel Pulse pokazuje historię zadań schedulera w jednym panelu (porównanie Pulse i Telescope).
Scheduler a kolejki — typowe błędy
- Ciężka praca w schedulerze — 5-minutowy import w
Schedule::command()blokuje cron; inne zadania czekają. Wzorzec: scheduler → dispatch do kolejki. - Brak
withoutOverlapping— przy zadaniu trwającym dłużej niż interwał uruchamiają się równoległe kopie; dwie synchronizacje naraz zdublują dane. - Brak
onOneServerprzy wielu serwerach — każde zadanie wykona się raz na każdym serwerze. Trzy serwery = trzy raporty i trzy maile. - Godziny bez strefy — serwer w UTC, zadanie
dailyAt('06:00')odpala się o 8:00 czasu polskiego (albo o 4:00 przy zmianie czasu). Ustawtimezone('Europe/Warsaw'). - Scheduler bez workerów — zadania
Schedule::job()kolejkują się, ale nikt ich nie wykonuje. Sprawdźsupervisorctl statusi długość kolejki.
Najczęściej zadawane pytania
Czy Laravel Scheduler wymaga crona?
Tak — jeden wpis * * * * * php artisan schedule:run na serwerze (albo na jednym z serwerów klastra). Laravel Scheduler to warstwa decyzyjna nad cronem: cron co minutę pyta „co teraz uruchomić", a odpowiedź siedzi w kodzie aplikacji. W development zamiast crona działa php artisan schedule:work.
Jaka jest różnica między schedulerem a kolejką?
Scheduler odpowiada na pytanie kiedy (co godzinę, o 6:00, w poniedziałki), kolejka na pytanie jak wykonać pracę poza cyklem żądania (w tle, z retry). Wzorzec produkcyjny łączy oba: scheduler decyduje „teraz", kolejkuje zadanie, a worker je wykonuje z całą obsługą błędów.
Jak uniknąć nakładających się zadań cyklicznych?
Metodą withoutOverlapping() — zadanie zakłada blokadę w cache i kolejne uruchomienia są pomijane, dopóki poprzednie trwa. Podaj limit minut (withoutOverlapping(10)), żeby zawieszona blokada nie zablokowała zadania na zawsze. Blokada wymaga współdzielonego cache (Redis), jeśli aplikacja działa na wielu serwerach.
Jak przetestować zadanie schedulera lokalnie?
php artisan schedule:list pokaże definicje, php artisan schedule:test uruchomi wybrane zadanie ręcznie, a php artisan schedule:work uruchomi lokalny demon bez konfigurowania crona. Samą komendę możesz zawsze wywołać bezpośrednio: php artisan reports:daily.
Czy zadania schedulera działają podczas maintenance mode?
Domyślnie nie — Laravel wstrzymuje scheduler w trybie maintenance. Jeśli zadanie musi działać mimo przestoju (np. synchronizacja z ERP), dodaj ->evenInMaintenanceMode().
👉 Zadania cron nakładają się, raporty wysyłają się podwójnie, a cron blokuje się na godzinę? To klasyczne błędy konfiguracji schedulera. Opisz projekt na kontakt@informatix.com.pl lub przez formularz — uporządkujemy zadania cykliczne i kolejki.
📖 Zobacz również: Kolejki w Laravel i Laravel Horizon — poradnik, Supervisor i systemd dla kolejek Laravel, queue:work vs queue:listen oraz usługa Laravel development.