Proste wzorce kolejek zadań w tle dla e-maili i webhooków
Poznaj proste wzorce kolejek zadań w tle do wysyłki e-maili, uruchamiania raportów i dostarczania webhooków z retry, backoff i obsługą dead-letter, bez ciężkich narzędzi.

Dlaczego potrzebujesz zadań w tle (i dlaczego szybko robi się bałagan)
Wszystko, co może trwać dłużej niż sekundę lub dwie, nie powinno działać w ramach żądania użytkownika. Wysyłanie e-maili, generowanie raportów i dostarczanie webhooków zależą od sieci, usług zewnętrznych lub wolnych zapytań. Czasem się zatrzymują, zwracają błędy lub po prostu zajmują więcej czasu niż się spodziewasz.
Jeśli wykonujesz taką pracę, gdy użytkownik czeka, od razu to widać. Strony się zawieszają, przyciski „Zapisz” kręcą się, a żądania kończą się timeoutem. Retry mogą też dziać się w złym miejscu. Użytkownik odświeża stronę, load balancer próbuje ponownie albo frontend wysyła ponownie i kończysz z duplikatami e-maili, wielokrotnymi wywołaniami webhooków lub z dwoma uruchomieniami raportu konkurującymi o zasoby.
Zadania w tle rozwiązują to, utrzymując żądania małymi i przewidywalnymi: przyjmij akcję, zapisz zadanie do wykonania później, odpowiedz szybko. Zadanie wykonuje się poza ścieżką żądania, na warunkach które kontrolujesz.
Trudna część to niezawodność. Gdy praca wychodzi ze ścieżki żądania, nadal musisz odpowiedzieć na pytania takie jak:
- Co jeśli dostawca e-maili padnie na 3 minuty?
- Co jeśli endpoint webhooka zwraca 500 lub kończy się timeoutem?
- Co jeśli zadanie uruchomi się dwa razy?
- Jak zauważyć zablokowane zadania zanim zgłoszą to użytkownicy?
Wiele zespołów odpowiada dodając „ciężką infrastrukturę”: broker wiadomości, oddzielne floty workerów, dashboardy, alerty i playbooki. Te narzędzia są przydatne, kiedy naprawdę ich potrzebujesz, ale dodają też nowe elementy i nowe sposoby awarii.
Lepszy cel startowy to prostota: niezawodne zadania z wykorzystaniem elementów, które już masz. Dla większości produktów to oznacza kolejkę opartą na bazie danych plus mały proces worker. Dodaj jasną strategię retry i backoff oraz wzorzec dead-letter dla zadań, które ciągle zawodzą. Otrzymujesz przewidywalne zachowanie bez zobowiązania do złożonej platformy od pierwszego dnia.
Nawet jeśli szybko budujesz z użyciem narzędzia napędzanego czatem jak Koder.ai, to rozdzielenie nadal ma sens. Użytkownicy powinni dostać szybką odpowiedź teraz, a system powinien dokończyć powolną, zawodną pracę bezpiecznie w tle.
Co to jest kolejka prostymi słowami
Kolejka to linia oczekujących zadań. Zamiast robić wolne lub zawodnę zadania podczas żądania użytkownika (wysłać mail, zbudować raport, wywołać webhook), zapisujesz mały rekord w kolejce i odsyłasz szybko. Później osobny proces pobiera ten rekord i wykonuje pracę.
Kilka słów, które często się pojawiają:
- Job: jedna jednostka pracy, np. „wyślij mail powitalny do użytkownika 123”.
- Worker: kod, który pobiera zadania i je wykonuje.
- Attempt: jedna próba uruchomienia zadania.
- Schedule: kiedy zadanie ma się uruchomić (teraz lub później).
- Queue: miejsce, gdzie zadania czekają, aż worker je pobierze.
Najprostszy przebieg wygląda tak:
-
Enqueue: aplikacja zapisuje rekord zadania (typ, payload, czas uruchomienia).
-
Claim: worker znajduje następne dostępne zadanie i „blokuje” je, aby tylko jeden worker je wykonał.
-
Run: worker wykonuje zadanie (wysyła, generuje, dostarcza).
-
Finish: oznacz je jako wykonane albo zapisz błąd i ustaw następny czas uruchomienia.
Jeśli wolumen zadań jest umiarkowany i już masz bazę danych, kolejka oparta na bazie często wystarcza. Łatwo ją zrozumieć, prosto debugować i pokrywa typowe potrzeby jak przetwarzanie e-maili czy niezawodność dostarczania webhooków.
Platformy streamingowe zaczynają mieć sens, gdy potrzebujesz bardzo dużej przepustowości, wielu niezależnych konsumentów lub możliwości odtwarzania ogromnej historii zdarzeń w wielu systemach. Jeśli uruchamiasz dziesiątki usług z milionami zdarzeń na godzinę, narzędzia jak Kafka mogą pomóc. Do tego momentu tabela w bazie plus pętla workera pokrywa wiele realnych przypadków.
Minimalne dane, które warto śledzić dla każdego zadania
Kolejka oparta na bazie danych pozostaje sensowna, jeśli każdy rekord zadania szybko odpowiada na trzy pytania: co zrobić, kiedy spróbować ponownie i co wydarzyło się ostatnio. Zrób to dobrze, a operacje staną się nudne (a to jest cel).
Co przechowywać w payload (a czego nie)
Przechowuj najmniejsze wejście potrzebne do wykonania pracy, a nie cały wygenerowany output. Dobre payloady to ID i kilka parametrów, np. { "user_id": 42, "template": "welcome" }.
Unikaj przechowywania dużych blobów (pełnych HTML e-maili, dużych danych raportu, ogromnych treści webhooków). Powoduje to szybszy wzrost bazy i utrudnia debugowanie. Jeśli zadanie potrzebuje dużego dokumentu, przechowaj referencję: report_id, export_id lub klucz do pliku. Worker pobierze pełne dane podczas działania.
Pola, które się opłacają
Przynajmniej zarezerwuj miejsce na:
- job_type + payload:
job_typewybiera handler (send_email, generate_report, deliver_webhook).payloadtrzyma małe wejścia jak ID i opcje. - status: trzymaj go jawnie (np.
queued,running,succeeded,failed,dead). - śledzenie prób:
attempt_countimax_attempts, by przestać retryować gdy ewidentnie nie zadziała. - pola czasowe:
created_atinext_run_at(kiedy staje się kwalifikowalne). Dodajstarted_atifinished_at, jeśli chcesz lepszą widoczność powolnych zadań. - idempotency + last error:
idempotency_key, by zapobiec podwójnym efektom, orazlast_error, by zobaczyć dlaczego zawiodło bez grzebania w logach.
Idempotencja brzmi poważnie, ale idea jest prosta: jeśli to samo zadanie uruchomi się dwa razy, druga próba powinna wykryć to i nie zrobić nic niebezpiecznego. Np. zadanie dostarczenia webhooka może używać klucza webhook:order:123:event:paid, żeby nie wysłać tego samego zdarzenia dwa razy, jeśli retry nachodzi na timeout.
Zbieraj też kilka podstawowych liczb od razu. Nie potrzebujesz dużego dashboardu na start, wystarczą zapytania, które powiedzą: ile zadań jest w kolejce, ile zawodzą i jaki jest wiek najstarszego zadania.
Krok po kroku: prosta kolejka w bazie, którą możesz zbudować dziś
Jeśli już masz bazę danych, możesz zacząć z kolejką w tle bez dodawania nowej infrastruktury. Zadania to wiersze, worker to proces, który stale wybiera należne wiersze i wykonuje pracę.
1) Stwórz tabelę jobs
Trzymaj tabelę małą i prostą. Chcesz wystarczająco pól, by uruchamiać, retryować i debugować zadania później.
CREATE TABLE jobs (
id bigserial PRIMARY KEY,
job_type text NOT NULL,
payload jsonb NOT NULL,
status text NOT NULL DEFAULT 'queued', -- queued, running, done, failed
attempts int NOT NULL DEFAULT 0,
next_run_at timestamptz NOT NULL DEFAULT now(),
locked_at timestamptz,
locked_by text,
last_error text,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX jobs_due_idx ON jobs (status, next_run_at);
Jeśli budujesz na Postgresie (częste przy back-endach w Go), jsonb to praktyczny sposób na przechowywanie danych zadania jak { "user_id":123,"template":"welcome" }.
2) Bezpieczne enqueue (szczególnie dla akcji użytkownika)
Gdy akcja użytkownika powinna wywołać zadanie (wysłać mail, odpal webhook), zapisz wiersz zadania w tej samej transakcji bazy co główna zmiana gdy to możliwe. Zapobiega to sytuacji „użytkownik utworzony, ale brak zadania” jeśli nastąpi crash zaraz po głównym zapisie.
Przykład: gdy użytkownik się rejestruje, wstaw w tej samej transakcji wiersz user i zadanie send_welcome_email.
3) Uruchom pętlę workera, która może skalować
Worker powtarza cykl: znajdź jedno należne zadanie, zarezerwuj je, przetwórz, oznacz za zakończone lub zaplanuj retry.
W praktyce oznacza to:
- Wybierz jedno zadanie gdzie
status='queued'inext_run_at <= now(). - Zarezerwuj je atomowo (w Postgresie
SELECT ... FOR UPDATE SKIP LOCKEDto częste podejście). - Ustaw
status='running',locked_at=now(),locked_by='worker-1'. - Przetwórz zadanie.
- Oznacz je jako zakończone (
done/succeeded) lub zapiszlast_errori ustaw następny termin próby.
Kilka workerów może działać jednocześnie. Krok claim zapobiega podwójnemu pobraniu.
4) Zamykanie bez psucia zadań
Przy zamykaniu procesu przestań pobierać nowe zadania, dokończ bieżące, a potem wyjdź. Jeśli proces umrze w trakcie pracy, użyj prostej zasady: traktuj zadania w running starsze niż timeout jako kwalifikujące się do ponownego wystawienia przez periodyczny „reaper”.
Jeśli budujesz w Koder.ai, ten wzorzec kolejki opartej na bazie to solidny domyślny wybór dla e-maili, raportów i webhooków zanim dodasz wyspecjalizowane usługi kolejkujące.
Retries i backoff, które nie powodują chaosu
Retry to sposób, w jaki kolejka radzi sobie z chaosem świata. Bez jasnych reguł retry zmienia się w hałaśliwą pętlę, która spamuje użytkowników, wali w API i ukrywa prawdziwy błąd.
Zacznij od decyzji, co retryować, a co porzucić od razu.
Retryuj problemy tymczasowe: timeouts sieciowe, 502/503, limity rate, krótkie problemy z bazą.
Porzucaj od razu, gdy zadanie nie ma szans: brak adresu e-mail, 400 od webhooka z powodu nieprawidłowego payloadu, czy żądanie raportu dla skasowanego konta.
Backoff to przerwa między próbami. Linearny backoff (5s, 10s, 15s) jest prosty, ale może tworzyć fale. Eksponencjalny backoff (5s, 10s, 20s, 40s) rozkłada obciążenie lepiej i jest zwykle bezpieczniejszy dla webhooków i dostawców zewnętrznych. Dodaj jitter (małe, losowe opóźnienie), żeby tysiąc zadań nie retryował dokładnie w tej samej sekundzie po awarii.
Reguły, które dobrze się sprawdzają:
- Retryuj tylko ewidentnie tymczasowe błędy (timeouts, 429, 5xx).
- Używaj eksponencjalnego backoff z jitterem.
- Ogranicz liczbę prób, potem oznacz zadanie jako failed.
- Ustaw timeout dla próby, żeby workerzy się nie blokowali.
- Każde zadanie powinno być idempotentne, żeby retry nie tworzyły duplikatów.
Max attempts ogranicza szkody. Dla wielu zespołów 5–8 prób wystarcza. Po tym przestań retryować i zaparkuj zadanie do przeglądu (dead-letter) zamiast robić pętlę w nieskończoność.
Timeouty zapobiegają „zombie” zadaniom. E-maile mogą mieć timeout 10–20 sekund na próbę. Webhooki często potrzebują krótszego limitu, np. 5–10 sekund, bo odbiorca może być niedostępny i chcesz iść dalej. Generowanie raportu może pozwolić na minuty, ale powinno mieć twardy cutoff.
Jeśli budujesz to w Koder.ai, traktuj should_retry, next_run_at i klucz idempotencji jako pola pierwszej klasy. Te drobne detale utrzymują system w ryzach, gdy coś idzie nie tak.
Dead-letter i proste operacje
Stan dead-letter to miejsce, gdzie trafiają zadania, gdy retry już nie ma sensu. Zmienia ciche porażki w coś, co można zobaczyć, wyszukać i na co zareagować.
Co zapisać na dead-letterze
Zapisz wystarczająco, by zrozumieć co się stało i móc odtworzyć zadanie bez zgadywania, ale uważaj na sekrety.
Trzymaj:
- Wejścia zadania (payload) dokładnie jak użyte, plus typ zadania i wersję
- Ostatni komunikat błędu i krótki stack trace (lub kod błędu)
- Liczbę prób, czas pierwszego uruchomienia, ostatnie uruchomienie i next_run_at (jeśli było zaplanowane)
- Tożsamość workera (nazwa usługi, host) i correlation ID dla logów
- Powód dead-letter (timeout, validation error, 4xx od dostawcy itp.)
Jeśli payload zawiera tokeny lub dane osobowe, zamaskuj je lub zaszyfruj przed zapisaniem.
Prosty workflow triage
Gdy zadanie trafi do dead-letter, podejmij szybką decyzję: retry, naprawić lub zignorować.
Retry to przypadki outage zewnętrznych usług i timeoutów. Naprawić to złe dane (brak e-maila, zły URL webhooka) lub błąd w kodzie. Ignorować powinno zdarzać się rzadko, ale ma sens, gdy zadanie jest już nieistotne (np. klient usunął konto). Jeśli ignorujesz, zapisz powód, żeby nie wyglądało jakby zadanie zniknęło.
Ręczne ponowne wystawienie jest najbezpieczniejsze, gdy tworzy nowe zadanie i pozostawia stary niezmienny. Oznacz dead-letter, kto i kiedy go ponownie wystawił oraz dlaczego, a potem enqueue’uj świeżą kopię z nowym ID.
Do alertów obserwuj sygnały oznaczające prawdziwy problem: szybki wzrost liczby dead-letterów, ten sam błąd powtarzający się w wielu zadaniach i stare zadania w kolejce, które nie są pobierane.
Jeśli używasz Koder.ai, snapshoty i rollback pomagają, gdy zła wersja wywoła nagły wzrost błędów — możesz szybko cofnąć zmiany i zbadać problem.
Na koniec dodaj zawory bezpieczeństwa na wypadek outage dostawcy. Ogranicz wysyłki na dostawcę i użyj circuit-breakera: jeśli endpoint webhooka mocno zawodzi, wstrzymaj nowe próby na krótki czas, żeby nie zalać ich (ani siebie) żądaniami.
Wzorce dla e-maili, raportów i webhooków
Kolejka działa najlepiej, gdy każdy typ zadania ma jasne reguły: co liczy się za sukces, co retryować i co nigdy nie powinno się zdarzyć dwa razy.
E-maile. Większość błędów e-maili jest tymczasowa: timeouts dostawcy, limity, krótkie awarie. Traktuj je jako retryowalne, z backoffem. Największe ryzyko to duplikaty, więc e-mail joby powinny być idempotentne. Przechowuj stabilny klucz deduplikujący, np. user_id + template + event_id i odmawiaj wysyłki jeśli ten klucz jest już oznaczony jako wysłany.
Warto też zapisać nazwę i wersję szablonu (lub hash wygenerowanego subject/body). Jeśli trzeba ponownie uruchomić joby, możesz wybrać, czy wysłać identyczną treść czy wygenerować ją od nowa z najnowszego szablonu. Jeśli dostawca zwraca message ID, zapisz go, by wsparcie mogło śledzić, co się stało.
Raporty. Raporty zawodzą inaczej. Mogą trwać minuty, natrafić na limity paginacji lub skończyć pamięć, jeśli wszystko robisz na raz. Podziel pracę na mniejsze kawałki. Częsty wzorzec: jedno zadanie „report request” tworzy wiele zadań „page” (lub „chunk”), z których każde przetwarza fragment danych.
Przechowuj wyniki do późniejszego pobrania zamiast trzymać użytkownika w oczekiwaniu. To może być tabela w bazie kluczowana przez report_run_id albo referencja pliku plus metadane (status, liczba wierszy, created_at). Dodaj pola postępu, żeby UI mogło pokazać „processing” vs „ready” bez zgadywania.
Webhooki. Webhooki to dostarczanie, a nie prędkość. Podpisuj każde żądanie (np. HMAC z shared secret) i dołączaj znacznik czasu, by zapobiec replayom. Retry tylko wtedy, gdy odbiorca może się później udać.
Prosty zestaw reguł:
- Retryuj przy timeoutach i 5xx, używając backoff i maksymalnej liczby prób.
- Traktuj większość 4xx jako błędy stałe i przestań retryować.
- Zapisuj ostatni kod statusu i krótki body odpowiedzi do debugowania.
- Użyj klucza idempotencji, żeby odbiorcy mogli bezpiecznie ignorować duplikaty.
- Ogranicz rozmiar payloadu i loguj to, co faktycznie wysłałeś.
Kolejność i priorytet. Większość zadań nie potrzebuje ścisłej kolejności. Kiedy kolejność ma znaczenie, zwykle dotyczy konkretnego klucza (na użytkownika, fakturę, endpoint webhooka). Dodaj group_key i pozwól na tylko jedno zadanie w locie na dany klucz.
Dla priorytetu oddziel pilne prace od wolnych. Duże backlogi raportów nie powinny opóźniać e-maili do resetu hasła.
Przykład: po zakupie enqueue’ujesz (1) e-mail potwierdzający zamówienie, (2) webhook do partnera i (3) zadanie aktualizacji raportu. E-mail retryuje szybko, webhook retryuje dłużej z backoffem, a raport uruchamia się później z niskim priorytetem.
Realistyczny przykład: rejestracja + webhook + nocny raport
Użytkownik rejestruje się w aplikacji. Trzy rzeczy powinny się wydarzyć, ale żadna nie powinna spowolnić strony rejestracji: wyślij mail powitalny, powiadom CRM webhookiem i dodaj użytkownika do nocnego raportu aktywności.
Co trafia do kolejki przy rejestracji
Zaraz po utworzeniu rekordu użytkownika zapisz trzy wiersze w tabeli kolejki. Każdy wiersz ma typ, payload (np. user_id), status, liczbę prób i timestamp next_run_at.
Typowy cykl życia wygląda tak:
queued: utworzone i czekające na workerarunning: worker je zarezerwowałsucceeded: zakończone, koniec pracyfailed: nie powiodło się, zaplanowane ponownie lub brak próbdead: za dużo nieudanych prób, wymaga uwagi człowieka
Job powitalny ma klucz idempotencji jak welcome_email:user:123. Przed wysyłką worker sprawdza tabelę zakończonych kluczy idempotencji (lub wymusza unikalność). Jeśli zadanie uruchomi się ponownie z powodu crashtu, druga próba zauważy klucz i pominie wysyłkę. Brak podwójnego maila powitalnego.
Awaria i jak się z tego wyjście
Teraz endpoint CRM jest niedostępny. Zadanie webhooka kończy się timeoutem. Worker planuje retry z backoffem (np. 1 minuta, 5 minut, 30 minut, 2 godziny) plus jitter, żeby wiele zadań nie retryowało dokładnie w tej samej sekundzie.
Po przekroczeniu max attempts zadanie trafia do dead. Użytkownik się zarejestrował, dostał mail powitalny, a zadanie nocnego raportu może działać normalnie. Tylko powiadomienie CRM jest utknięte i jest widoczne.
Następnego ranka support (lub on-call) może to obsłużyć bez grzebania godzinami w logach:
- Filtruj dead jobs po typie (np.
webhook.crm). - Przeczytaj ostatni komunikat błędu i potwierdź, że payload wygląda poprawnie.
- Zweryfikuj, że CRM jest z powrotem dostępny.
- Requeue’uj zadanie (dead -> queued, zresetuj attempts) lub tymczasowo wyłącz ten cel.
Jeśli budujesz aplikacje na platformie jak Koder.ai, ten sam wzorzec się stosuje: utrzymuj flow użytkownika szybkim, przerzucaj efekty uboczne do jobów i sprawiaj, żeby błędy było łatwo podejrzeć i uruchomić ponownie.
Częste błędy, które czynią kolejki zawodnymi
Najszybszy sposób, by zepsuć kolejkę, to traktować ją jako opcjonalną. Zespoły często zaczynają od „tym razem po prostu wyślij e-mail w żądaniu”, bo wydaje się to prostsze. Potem to się rozrasta: reset hasła, potwierdzenia, webhooki, eksporty raportów. Wkrótce aplikacja jest wolna, timeouts rosną, a jakikolwiek problem z zewnętrznym serwisem staje się twoim outage.
Inna pułapka to pominięcie idempotencji. Jeśli zadanie może się uruchomić dwa razy, nie powinno tworzyć dwóch rezultatów. Bez idempotencji retry prowadzą do duplikatów e-maili, powtarzających się zdarzeń webhook itp.
Trzeci problem to brak widoczności. Jeśli o błędach dowiadujesz się tylko z ticketów supportu, kolejka już szkodzi użytkownikom. Nawet podstawowy widok wewnętrzny pokazujący liczbę zadań po statusie i przeszukiwalny last_error oszczędza czas.
Zabójcy niezawodności, na które warto uważać
Kilka problemów pojawia się szybko, nawet w prostych kolejkach:
- Retry natychmiast po błędzie. Jeśli dostawca jest niedostępny, szybkie retry tworzy twój własny spike ruchu.
- Mieszanie powolnych zadań z pilnymi. 10-minutowy raport może blokować mail weryfikacyjny.
- Traktowanie błędów jako tymczasowych wiecznie. Zadania, które nigdy nie zadziałają, kręcą się w kółko i ukrywają realne problemy.
- Brak wersjonowania payloadów. Jeśli zmienisz kształt zadania, stare mogą zacząć padać.
- Ignorowanie limitów rate. Kolejki mogą zalać dostawców, którzy cię throttlują.
Backoff zapobiega samozrobionym outage’om. Nawet podstawowy harmonogram jak 1 minuta, 5 minut, 30 minut, 2 godziny czyni awarie bezpieczniejszymi. Ustaw także limit prób, żeby złamane zadanie zatrzymało się i stało widoczne.
Jeśli budujesz na platformie jak Koder.ai, dobrze jest wypuścić te podstawy razem z funkcją, a nie tygodnie później jako sprzątanie.
Szybka checklist i następne kroki
Zanim dodasz więcej narzędzi, upewnij się, że podstawy działają. Kolejka oparta na bazie działa dobrze, gdy każde zadanie jest łatwe do zarezerwowania, retryowania i obejrzenia.
Szybka lista kontrolna niezawodności:
- Każde zadanie ma: id, typ, payload, status, attempts, max_attempts, run_at/next_run_at i last_error.
- Workerzy rezerwują zadania bezpiecznie (jeden worker = jedno zadanie) i odzyskują po crashu (timeout lock + reaper).
- Każde zadanie ma jasny timeout, więc zawieszona praca staje się retryowalna zamiast wisieć wiecznie.
- Retry są ograniczone, a opóźnienie rośnie (backoff), żeby uniknąć thundering herd.
- Jest stan dead-letter (lub tabela) i jasny sposób na ponowne uruchomienie lub odrzucenie zadań.
Następnie wybierz pierwsze trzy typy zadań i zapisz ich reguły. Na przykład: e-mail resetu hasła (szybkie retry, krótki max), nightly report (kilka retry, dłuższe timeouty), delivery webhook (więcej retry, dłuższy backoff, stop przy stałych 4xx).
Jeśli nie wiesz, kiedy kolejka w bazie przestaje wystarczać, obserwuj sygnały takie jak contention na poziomie wiersza przy wielu workerach, potrzeby ścisłej kolejności dla wielu typów zadań, duży fan-out (jedno zdarzenie wyzwala tysiące zadań) albo konsumcja cross-service, gdzie różne zespoły mają różnych workerów.
Jeśli chcesz szybki prototyp, możesz naszkicować flow w Koder.ai (koder.ai) w trybie planowania, wygenerować tabelę jobs i pętlę workera, a potem iterować ze snapshotami i rollbackem przed wdrożeniem.
Często zadawane pytania
When should I move work into a background job instead of doing it in the request?
Jeśli zadanie może zająć więcej niż sekundę-dwie albo zależy od wywołania sieciowego (dostawca e-maili, endpoint webhooka, wolne zapytanie), przenieś je do background job.
Skup żądanie użytkownika na walidacji danych, zapisaniu głównej zmiany, zapisaniu zadania do kolejki i szybkim zwróceniu odpowiedzi.
Is a database-backed queue really enough, or do I need a message broker?
Zacznij od kolejki opartej na bazie danych gdy:
- Już używasz PostgreSQL i ruch jest umiarkowany
- Chcesz najprostsze rozwiązanie, które łatwo debugować
- Jedna usługa odpowiada za enqueue i przetwarzanie
Dodaj broker/streaming gdy potrzebujesz bardzo dużej przepustowości, wielu niezależnych konsumentów lub możliwości odtwarzania zdarzeń między usługami.
What fields should every job record have?
Śledź podstawowe informacje, które odpowiadają: co zrobić, kiedy spróbować ponownie i co się wydarzyło ostatnio.
Praktyczne minimum:
job_type,payloadstatus(queued,running,succeeded,failed,dead)attempts,max_attemptsnext_run_at, pluscreated_atlocked_at,locked_bylast_erroridempotency_key(lub inny mechanizm deduplikacji)
What should I put in the job payload (and what should I avoid)?
Przechowuj wejścia, nie duże wyjścia.
Dobre payloady:
- ID i małe opcje (np.
user_id,template,report_id)
Unikaj:
- Pełnego wygenerowanego HTML e-maili
- Dużych blobów z danymi raportów
- Ogromnych treści webhooków
Jeśli zadanie potrzebuje dużych danych, przechowaj referencję (np. report_run_id lub klucz pliku) i pobierz zawartość w workerze.
How do multiple workers avoid picking the same job?
Kluczem jest atomowy krok „claim”, żeby dwóch workerów nie pobrało tego samego zadania.
Popularne podejście w Postgres:
- Wybierz należne wiersze z blokadą (np.
FOR UPDATE SKIP LOCKED) - Natychmiast oznacz zadanie jako
runningi ustawlocked_at/locked_by
Dzięki temu workery mogą skalować się poziomo bez podwójnego przetwarzania.
How do I prevent duplicate emails or duplicate webhook deliveries?
Zakładaj, że zadania czasem się uruchomią dwukrotnie (crashe, timeouts, retry). Uczyń efekt uboczny bezpiecznym.
Proste wzorce:
- Dodaj
idempotency_keyjakwelcome_email:user:123 - Wymuś unikalność (unikalny indeks lub osobna tabela z kluczami zakończonych zadań)
- Przy powtórnym uruchomieniu wykryj klucz i pomiń wysyłkę/dostarczenie
To szczególnie ważne dla e-maili i webhooków, żeby uniknąć duplikatów.
What retry and backoff strategy should I start with?
Użyj jasnej polityki i trzymaj się jej:
- Retry tylko przy tymczasowych błędach (timeouts, 429, 5xx)
- Eksponencjalny backoff z jitterem
- Limit prób (zwykle 5–8)
- Timeout per-attempt, by workerzy się nie zawieszali
Fail fast przy błędach stałych (brak adresu e-mail, nieprawidłowy payload, większość 4xx z webhooków).
What is a dead-letter job, and when should I use it?
Dead-letter oznacza „przestań retryować i pokaż to”. Używaj, gdy:
- Przekroczono
max_attempts - Błąd jest ewidentnie permanentny
- Retry powodowałby szkodę (spam, powtarzające się złe webhooki)
Zapisz wystarczający kontekst do działania:
- typ zadania, payload (po redakcji jeśli trzeba)
- liczba prób i znaczniki czasu
last_errori ostatni kod statusu (dla webhooków)
Przy replayu tworzy się nowego joba, pozostawiając dead-letter niezmieniony.
How do I deal with jobs that get stuck in running after a crash?
Radź sobie ze „zawieszonymi” zadaniami dwoma zasadami:
- Każda próba ma timeout (żeby praca nie trwała w nieskończoność)
- Periodyczny reaper wykrywa
runningstarsze niż próg i re-queue’uje je (lub oznacza jako failed)
To pozwala systemowi wrócić do działania po crashu workera bez ręcznego sprzątania.
How do I handle priority and ordering (so reports don’t delay critical emails)?
Oddziel powolne od pilnych prac:
- Umieść pilne zadania (reset hasła, e-maile weryfikacyjne) w kolejce wysokiego priorytetu
- Ciężkie zadania (duże raporty) w kolejce niskiego priorytetu
Jeśli kolejność ma znaczenie, zwykle dotyczy klucza (per user, per endpoint). Dodaj group_key i pozwól na jedno zadanie w locie na klucz, by zachować lokalną kolejność bez globalnej blokady.