ix
ix

informatix

EN version Porozmawiajmy o projekcie
Strona główna / Blog / Job batching, chains i rate limiting w Laravel — wzorce [202...
• 9 min czytania •

Job batching, chains i rate limiting w Laravel — wzorce [2026]

Import 10 000 wierszy jako batch z paskiem postępu, faktura → PDF → mail jako chain i rate limiting, który chroni API partnera przed kaskadą 429. Trzy wzorce, które odróżniają „używam kolejek" od „używam kolejek świadomie".

Pojedyncze zadanie w kolejce to dopiero początek. Prawdziwe systemy potrzebują trzech wzorców: batchingu (setki zadań z jednym zbiorczym wynikiem — importy, kampanie), łańcuchów (zadania wykonywane po kolei, gdzie każde dostaje wynik poprzedniego — faktura → PDF → mail) i rate limitingu (ochrona zewnętrznych API przed zalaniem). Ten poradnik pokazuje wszystkie trzy z produkcyjnymi przykładami.

Najważniejsze: Najważniejsze — Bus::batch() grupuje zadania i daje jedno powiadomienie o zakończeniu całości plus postęp ($batch->progress()). Bus::chain() wykonuje zadania sekwencyjnie i przerywa łańcuch na pierwszym błędzie. Rate limiting przez middleware RateLimited chroni zewnętrzne API — bez niego 10 workerów wyśle 10 równoległych żądań i dostanie 429.

Job batching — setki zadań, jeden wynik

Import 10 000 wierszy CSV jako 10 000 osobnych zadań?

Tak, i to jest właściwe podejście — ale potrzebujesz wiedzieć, kiedy import się skończył i czy coś się nie powiodło. Batch rozwiązuje oba problemy:

use App\Jobs\ImportRow;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;

$batch = Bus::batch(
    $rows->map(fn ($row) => new ImportRow($row))
)->then(function (Batch $batch) {
    // wszystkie zadania zakończone sukcesem
    Notification::send($user, new ImportCompleted($batch->id));
})->catch(function (Batch $batch, Throwable $e) {
    // pierwszy błąd w batchu — tu logujesz i alertujesz
    Log::error('Import failed', ['batch' => $batch->id, 'error' => $e->getMessage()]);
})->finally(function (Batch $batch) {
    // zawsze — sukces czy błąd
    Cache::forget('import-running');
})->name('Import produktów')
  ->allowFailures()          // jeden błąd nie zatrzymuje całego batcha
  ->dispatch();

Kluczowe właściwości batcha

Metoda / właściwość Znaczenie
then() Callback po sukcesie całego batcha
catch() Callback przy pierwszym błędzie
finally() Zawsze na końcu (sukces lub błąd)
allowFailures() Błędy pojedynczych zadań nie przerywają batcha
$batch->progress() Procent ukończenia (0–100)
$batch->processedJobs() Liczba przetworzonych zadań
$batch->failedJobs Liczba nieudanych
$batch->cancel() Anulowanie batcha w trakcie

Postęp pokazujesz użytkownikowi w interfejsie — pobierasz batch po ID i renderujesz pasek

$batch = Bus::findBatch($batchId);

return [
    'progress' => $batch->progress(),          // 0-100
    'processed' => $batch->processedJobs(),    // 342
    'total' => $batch->totalJobs,              // 1000
    'failed' => $batch->failedJobs,            // 0
];

Gdzie trafiają batche w bazie? Tabele job_batches — wymaga migracji php artisan make:queue-batches-table. Bez niej batch rzuci wyjątek przy dispatchu. Warto też pamiętać, że batch działa najlepiej na Redisie: sterownik database obsługuje batche, ale pod obciążeniem kolejkuje wolniej.

Kiedy batch zamiast pętli dispatchów? Zawsze, gdy potrzebujesz wiedzieć, że „całość się skończyła" — importy, masowe aktualizacje cen, wysyłka kampanii, przetwarzanie plików. Pętla foreach { dispatch() } nie daje żadnej z tych informacji.

Job chains — sekwencje z przekazywaniem wyniku

Chain wykonuje zadania po kolei, a każde następne może dostać wynik poprzedniego. Typowy scenariusz fakturowy

use App\Jobs\{FetchInvoice, GenerateInvoicePdf, SendInvoiceEmail};
use Illuminate\Support\Facades\Bus;

Bus::chain([
    new FetchInvoice($order),
    new GenerateInvoicePdf($order),
    new SendInvoiceEmail($order),
])->catch(function (Throwable $e) {
    Log::error('Invoice chain failed', ['error' => $e->getMessage()]);
})->dispatch();

Trzy właściwości łańcucha, które trzeba znać

  • Przerwanie na pierwszym błędzie — jeśli GenerateInvoicePdf rzuci wyjątek, SendInvoiceEmail się nie wykona. Nie wyślesz maila z PDF-em, którego nie ma. To najczęściej pożądane zachowanie.
  • Współdzielony kontekst — kolejne zadania dostają wynik poprzedniego przez parametr konstruktora; łańcuch przekazuje obiekt dalej (wzorzec „przekaż wynik dalej").
  • Jeden catch() dla całego łańcucha — obsługujesz błąd na końcu, nie w każdym zadaniu osobno.

Chain i batch można łączyć: batch, którego zadania to łańcuchy (Bus::batch([...])->then(...) z chainami w środku) — ale zanim to zrobisz, zapytaj, czy problem nie jest prostszy. Dwa poziomy zagnieżdżenia to zwykle sygnał, że proces warto rozbić na mniejsze.

Rate limiting — ochrona zewnętrznych API

  • Scenariusz: 10 workerów, każdy przetwarza zadania, które wołają API partnera z limitem 30 żądań na minutę. Bez ograniczenia 10 procesów wyśle 10 równoległych żądań w tej samej sekundzie, partner odpowie 429, zadania wejdą w retry i po kilku minutach cała kolejka jest zajęta przez zadania, które i tak nie mogą się wykonać. Klasyczna kaskada awarii.

Rozwiązanie: middleware RateLimited

use Illuminate\Queue\Middleware\RateLimited;

class SyncProductToPartner implements ShouldQueue
{
    public function middleware(): array
    {
        return [new RateLimited('partner-api')];
    }
}

// AppServiceProvider::boot() — definicja limitu
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

RateLimiter::for('partner-api', function (object $job) {
    return Limit::perMinute(30)->by('partner-api');
});
  • Co się dzieje: zadanie, które nie dostanie „przepustki" w limicie, wraca do kolejki z opóźnieniem i spróbuje później — bez zużywania próby retry i bez błędu. Praca nie ginie, tylko czeka na swoją kolej.

Można też różnicować limit per partner (klucz po ID klienta), gdy każdy klient ma własny limit

RateLimiter::for('partner-api', function (object $job) {
    return Limit::perMinute(30)->by($job->partnerId);
});

Release i opóźnienia — kontrola nad zwracaniem zadania

Zadanie odrzucone przez limiter wraca do kolejki domyślnie po ~60 sekundach. Możesz to kontrolować i różnicować zachowanie:

public function middleware(): array
{
    return [
        (new RateLimited('partner-api'))->releaseAfter(30),   // wróć po 30 s
        // albo: ->dontRelease() — zadanie NIE wraca do kolejki (rzuca wyjątek i idzie do failed_jobs)
    ];
}

dontRelease() przydaje się, gdy przekroczenie limitu oznacza błąd konfiguracji, a nie chwilowy szczyt — chcesz wtedy głośny błąd w failed_jobs, a nie ciche krążenie zadania. Domyślne „release" (zwróć i spróbuj później) jest właściwe dla chwilowych szczytów ruchu.

W komendach i kodzie poza kolejką ten sam mechanizm obsługuje Redis::throttle() — fluentny builder limitu używany wewnątrz zadań lub komend:

Redis::throttle('partner-api')->allow(30)->every(60)->then(
    fn () => $this->syncWithPartner(),
    fn () => $this->release(60),   // limit wyczerpany — zwróć zadanie
);

W praktyce 90% przypadków pokrywa RateLimited z named limiterem — po Redis::throttle() sięgaj, gdy potrzebujesz limitu ad-hoc bez definiowania go w providerze.

Uwaga: Rate limiting bez allowFailures() albo bez obsługi „zwróconego" zadania potrafi zamaskować problem: kolejka rośnie, zadania krążą, a w logach nie ma błędu. Monitoruj długość kolejki (Horizon) i czas oczekiwania — jeśli rośnie mimo sprawnych workerów, sprawdź, czy limiter nie dławi ruchu bardziej, niż zakładałeś.

Który wzorzec wybrać — drzewko decyzyjne

Potrzebujesz… Wzorzec
Setki niezależnych zadań + wiedzieć, kiedy całość się skończyła Batch
Zadania wykonywane po kolei, zależne od siebie Chain
Ochrony zewnętrznego API przed zalaniem Rate limiting
Jednego zadania wykonanego w tle Zwykły dispatch()
Powtarzania zadania w interwale Scheduler + Schedule::job()

Kolejność wdrażania w istniejącym projekcie: najpierw rate limiting (jeśli są zewnętrzne API — najczęstsze źródło awarii), potem batch dla procesów masowych, na końcu chainy dla wieloetapowych przepływów.

Testowanie batchy, chainów i limitów

Dyspozycję sprawdzasz przez Bus::fake()

Bus::fake();

$this->post('/imports', ['file' => $file]);

Bus::assertBatched(fn ($batch) => $batch->jobs->count() === 100);
Bus::assertChained([FetchInvoice::class, GenerateInvoicePdf::class]);

Limity testujesz trudniej (bo działają w warstwie kolejki) — najczęściej testem integracyjnym ze sterownikiem sync albo weryfikacją konfiguracji limitera. Szerzej o testowaniu kolejek piszę w poradniku Testowanie kolejek i jobów w Laravel.

Najczęściej zadawane pytania

Kiedy użyć batcha zamiast pętli dispatch()?

Gdy potrzebujesz wiedzieć, że całość się skończyła (powiadomienie, dalszy krok procesu) albo pokazać użytkownikowi postęp. Pętla foreach { dispatch() } nie daje żadnej z tych informacji. Batch dodaje też allowFailures(), cancel() i zbiorczą obsługę błędów.

Co się dzieje z łańcuchem, gdy jedno zadanie się nie powiedzie?

Łańcuch przerywa się na pierwszym błędzie — kolejne zadania się nie wykonają, a błąd trafia do catch() łańcucha (jeśli zdefiniowany) i do tabeli failed_jobs. To zachowanie chroni przed częściowymi stanami: nie wyślesz maila z fakturą, której nie udało się wygenerować.

Jak działa rate limiting w kolejkach Laravel?

Middleware RateLimited sprawdza named limiter (zdefiniowany w AppServiceProvider) przed wykonaniem zadania. Jeśli limit jest wyczerpany, zadanie wraca do kolejki z opóźnieniem — bez zużywania próby retry i bez błędu. Praca czeka, aż limit się „odnowi", a nie przepada.

Czy batch i chain można łączyć?

Technicznie tak — batch zadań, z których każde jest łańcuchem. W praktyce dwa poziomy zagnieżdżenia trudno się debuguje i rzadko jest potrzebne. Jeśli proces wymaga takiej struktury, rozważ rozbicie go na dwa etapy: batch kończy się, a jego callback dispatchuje kolejny etap.

Ile kosztuje wdrożenie batchingu w istniejącym projekcie?

Samo dodanie batchy to zwykle 1–2 dni pracy: migracja tabeli job_batches, refaktor pętli dispatchów na Bus::batch(), callbacki i podgląd postępu w UI. Największy koszt to zwykle warstwa interfejsu (pasek postępu, obsługa błędów częściowych), nie sam mechanizm kolejki.


👉 Importy blokują aplikację, a integracje dostają 429? Batching i rate limiting rozwiązują oba problemy. Opisz projekt na kontakt@informatix.com.pl lub przez formularz — wycena jest bezpłatna.

📖 Zobacz również: Kolejki w Laravel i Laravel Horizon — poradnik, testowanie kolejek, integracje API i webhooki oraz usługa Laravel development.

Darmowy PDF

Checklista wymagań do wyceny aplikacji

12 pytań kontrolnych, które warto przygotować przed rozmową z wykonawcą — szybsza wycena, mniej ukrytych kosztów.

Powiązane wpisy

09.10.2026

Laravel Horizon i kolejki w Laravel — kompletny poradnik [2026]

Kolejki w Laravel przenoszą ciężkie operacje poza cykl żądania HTTP, a Laravel Horizon daje monitoring i autoskalowanie workerów. Kompletny poradnik: wybór sterownika, konfiguracja produkcyjna, retry i idempotencja, testy, koszty wdrożenia.

Czytaj dalej

21.10.2026

Laravel Pulse i Telescope — monitoring aplikacji [2026]

Horizon odpowiada „co robią kolejki", Pulse „jak czuje się aplikacja", a Telescope „co się stało w tym żądaniu". Kompletny przewodnik: karty Pulse, bezpieczeństwo Telescope na produkcji i minimalny zestaw monitoringowy.

Czytaj dalej