Przejdź do treści
Workin'FlowsBlog → Idempotency w webhookach n8n
n8n

Idempotency w webhookach n8n: ta sama płatność tylko raz

Adrian Krawczyk opublikowane ostatnia aktualizacja 6 min czytania

Gdy Stripe wysyła ten sam webhook trzeci raz, Twoja architektura musi krzyczeć STOP. Webhook bez idempotency to nie automatyzacja, to loteria z ładnym UI.

Stripe potrafi wysłać ten sam webhook więcej niż raz. To nie bug. To normalka. Problem zaczyna się wtedy, gdy Twój workflow w n8n traktuje każde wejście jak nowe zdarzenie. Wtedy jeden klient dostaje dostęp dwa razy, faktura leci drugi raz, a księgowość robi minę jak recepcjonista, któremu ta sama osoba melduje się na ten sam pokój trzy razy.

W tym artykule rozkminimy, jak zrobić idempotency w webhookach n8n tak, żeby ta sama płatność nie została przetworzona dwa razy. Bez czarów, bez wielkiej architektury z kosmosu. Będzie klucz, tabela w bazie, logika workflow, edge case'y i przykład pod Stripe. Piszę to z perspektywy warsztatu procesu, bo brak idempotency wychodzi zawsze wtedy, gdy już jest ruch. Czyli gdy najmniej chcesz gasić pożar.

Czym jest idempotency w webhookach n8n?

Idempotency w webhookach n8n oznacza, że to samo zdarzenie może przyjść wiele razy, ale efekt biznesowy wykona się tylko raz.

Przykład prosty jak barszcz. Stripe wysyła event:

{
  "id": "evt_123",
  "type": "checkout.session.completed",
  "data": {
    "object": {
      "id": "cs_test_456",
      "payment_intent": "pi_789",
      "customer_email": "klient@example.com"
    }
  }
}

Jeśli ten event przyjdzie trzy razy, workflow bez zabezpieczenia uruchomi się trzy razy:

  1. Tworzysz użytkownika trzy razy.
  2. Wysyłasz trzy maile powitalne.
  3. Nadajesz dostęp trzy razy.
  4. Robisz trzy wpisy w CRM.
  5. Księgowość zaczyna jojczeć, bo ma duble faktur.

Z idempotency pierwszy webhook przechodzi, a drugi i trzeci dostają grzeczne "to już było" i kończą bez efektów ubocznych.

Dlaczego Stripe wysyła ten sam webhook kilka razy?

Bo Stripe gwarantuje dostarczenie w modelu at-least-once, czyli "co najmniej raz". To świadoma decyzja projektowa, nie usterka. Lepiej dostarczyć event dwa razy niż zgubić go raz.

Ponowna wysyłka leci, gdy:

Wniosek jest prosty: skoro nadawca może powtórzyć, to odbiorca musi umieć rozpoznać powtórkę. Ten obowiązek jest po Twojej stronie, nie po stronie Stripe.

Jaki klucz idempotency wybrać?

Klucz idempotency to coś, co jednoznacznie identyfikuje zdarzenie. Dwa rozsądne wybory:

Wariant prosty: identyfikator eventu Stripe, czyli pole id (np. evt_123). Stripe gwarantuje, że jest unikalny dla każdego zdarzenia. Dla jednego źródła płatności to wystarcza.

Wariant pod wielu dostawców: hash z połączenia źródła, identyfikatora i typu. Stosuję go, gdy webhooki sypią się z kilku systemów i nie chcę kolizji kluczy między nimi:

idempotency_key = sha256(provider + ":" + event_id + ":" + event_type)
// np. sha256("stripe:evt_123:checkout.session.completed")
Z doświadczenia Nie buduj klucza z danych biznesowych typu email albo kwota. Dwa różne zdarzenia mogą mieć ten sam email, a jedno zdarzenie może przyjść z różną kwotą po korekcie. Klucz ma identyfikować zdarzenie, nie klienta.

Tabela w bazie i atomowy INSERT

Sednem idempotency jest jedna tabela z ograniczeniem UNIQUE na kluczu. To baza, nie n8n, pilnuje, żeby klucz wpadł tylko raz, nawet gdy dwa webhooki przyjdą w tej samej milisekundzie.

CREATE TABLE processed_events (
  idempotency_key TEXT PRIMARY KEY,
  status          TEXT NOT NULL DEFAULT 'processing',
  event_type      TEXT,
  created_at      TIMESTAMPTZ NOT NULL DEFAULT now(),
  processed_at    TIMESTAMPTZ
);

Trik polega na tym, żeby próba zapisu i sprawdzenie "czy już było" to była jedna, niepodzielna operacja:

INSERT INTO processed_events (idempotency_key, event_type)
VALUES ($1, $2)
ON CONFLICT (idempotency_key) DO NOTHING
RETURNING idempotency_key;

Jeśli zapytanie zwróci wiersz, to znaczy, że ten klucz wpadł teraz pierwszy raz, więc możesz przetwarzać. Jeśli nie zwróci nic, klucz już istniał, więc przerywasz. Bazodanowy UNIQUE rozwiązuje wyścig dwóch równoległych webhooków za Ciebie, bez kombinowania z blokadami w n8n.

Workflow w n8n krok po kroku

Tak układam ten przepływ w n8n:

  1. Webhook node przyjmuje POST od Stripe.
  2. Weryfikacja podpisu Stripe (Stripe-Signature) i sprawdzenie świeżości timestampu, max kilka minut, żeby odbić ataki typu replay.
  3. Function node buduje idempotency_key z id eventu.
  4. Postgres node robi INSERT ... ON CONFLICT DO NOTHING RETURNING.
  5. IF node: jeśli nic nie wróciło, kończ z odpowiedzią 200 "already processed". To kluczowy krok, idempotency siedzi na początku, nie na końcu.
  6. Logika biznesowa: nadaj dostęp, wystaw fakturę, wyślij maila. Tylko teraz.
  7. Postgres node: UPDATE statusu na success i zapis processed_at.
Idempotency to nie ostatni węzeł, który łapie duble. To pierwszy węzeł, który ich nie wpuszcza.

Statusy: processing, success, failed

Status w tabeli to nie ozdoba. To on decyduje, co robi kolejna powtórka eventu.

Edge case, który gryzie po nocach Co, jeśli worker padnie między zapisem processing a końcem logiki? Klucz zostaje na zawsze w processing i blokuje retry. Dlatego trzymam prostą regułę: event w processing starszy niż np. 15 minut traktuję jak porzucony i pozwalam go ponowić. Bez tego jedna padnięta egzekucja zamraża płatność na amen.

Najczęstsze błędy, które widzę

Najczęstsze pytania

Czym jest idempotency w webhookach n8n?

To zasada, że to samo zdarzenie może przyjść wiele razy, ale efekt biznesowy wykona się tylko raz. Webhook na pierwszym kroku zapisuje klucz zdarzenia do tabeli z ograniczeniem UNIQUE i przerywa, gdy klucz już istnieje. Bez duplikatu płatności, faktury czy dostępu.

Dlaczego Stripe wysyła ten sam webhook kilka razy?

Bo działa w modelu at-least-once. Jeśli endpoint odpowie wolno, zwróci 5xx albo potwierdzenie 2xx zgubi się w sieci, Stripe ponowi wysyłkę. To projekt, nie błąd, więc to odbiorca musi rozpoznać powtórkę.

Jaki klucz idempotency wybrać dla Stripe?

Dla jednego źródła wystarczy identyfikator eventu Stripe (pole id, np. evt_123). Przy wielu dostawcach buduję klucz jako sha256 z połączenia źródła, identyfikatora i typu eventu, żeby uniknąć kolizji między systemami.

Masz webhooki, które dotykają płatności?

Zaczynam od warsztatu procesu (90 minut, bezpłatny). Rozrysujemy przepływ, znajdziemy miejsca, gdzie powtórka eventu robi dubla, i ustawimy idempotency tam, gdzie trzeba. Bez wciskania jednego słusznego stacku.

Zamów warsztat → Lub napisz bezpośrednio.
Nota o procesie: Szkic artykułu powstał z udziałem agenta AI w ramach Workin'Agency. Redakcja merytoryczna, weryfikacja techniczna i zatwierdzenie: Adrian Krawczyk. (AI Act, Art. 50)