Checklist độ tin cậy cho webhook: thử lại, bất biến (idempotency) và phát lại
Checklist thực tế về độ tin cậy webhook: thử lại, tính bất biến (idempotency), nhật ký phát lại và giám sát cho webhook inbound và outbound khi đối tác gặp sự cố.

Tại sao webhook lại cảm thấy không đáng tin trong dự án thực
Webhook là một thỏa thuận đơn giản: một hệ thống gửi yêu cầu HTTP đến hệ thống khác khi có điều gì đó xảy ra. "Đơn hàng đã gửi", "ticket được cập nhật", "thiết bị mất kết nối". Về cơ bản đó là một thông báo đẩy giữa các app, gửi qua web.
Chúng có vẻ đáng tin trong demo vì đường đi lý tưởng thì nhanh và sạch. Trong công việc thực, webhook đứng giữa những hệ thống bạn không kiểm soát: CRM, nhà cung cấp vận chuyển, help desk, công cụ marketing, nền tảng IoT, thậm chí các app nội bộ do team khác sở hữu. Ngoại trừ các hệ thống thanh toán, bạn thường không còn được đảm bảo giao hàng chín chắn, schema sự kiện ổn định và hành vi retry nhất quán.
Dấu hiệu đầu tiên thường hơi khó hiểu:
- Sự kiện trùng lặp (cùng một cập nhật đến hai lần)
- Sự kiện mất (có thay đổi nhưng bạn không nhận được)
- Trễ (một cập nhật đến muộn hàng phút hoặc hàng giờ)
- Sự kiện sai thứ tự (một cập nhật "closed" đến trước "opened")
Các hệ thống bên thứ ba chập chờn làm mọi thứ có vẻ ngẫu nhiên bởi vì lỗi không luôn luôn rõ ràng. Nhà cung cấp có thể timeout nhưng vẫn xử lý yêu cầu của bạn. Một load balancer có thể đóng kết nối sau khi sender đã retry. Hoặc hệ thống của họ có thể sập thoáng qua rồi sau đó gửi một loạt sự kiện cũ cùng lúc.
Hãy tưởng tượng một đối tác vận chuyển gửi webhook "delivered". Một ngày receiver của bạn chậm 3 giây, họ retry. Bạn nhận hai lần delivered, khách hàng nhận hai email, support bối rối. Ngày sau họ bị outage và không retry, vậy "delivered" không đến và dashboard của bạn đứng im.
Độ tin cậy webhook ít liên quan tới một request hoàn hảo mà hơn là thiết kế cho thực tế lộn xộn: retry, idempotency và khả năng phát lại cũng như xác minh sau này.
Ba khối xây dựng: retry, idempotency, replay
Webhook có hai hướng. Inbound webhooks là các cuộc gọi bạn nhận từ người khác (nhà cung cấp thanh toán, CRM, công cụ vận chuyển). Outbound webhooks là các cuộc gọi bạn gửi đến khách hàng hoặc đối tác khi có thay đổi trong hệ thống. Cả hai đều có thể thất bại vì những lý do không liên quan đến mã của bạn.
Retry là những gì xảy ra sau thất bại. Sender có thể retry vì timeout, lỗi 500, mất kết nối hoặc không có phản hồi đủ nhanh. Retry tốt là hành vi được mong đợi, không phải trường hợp hiếm. Mục tiêu là đưa sự kiện qua mà không làm ngập receiver hoặc tạo ra các side effect trùng lặp.
Idempotency là cách bạn làm cho trùng lặp trở nên an toàn. Nó có nghĩa là "chỉ làm một lần, ngay cả khi nhận nhiều lần". Nếu cùng một webhook đến lần nữa, bạn phát hiện và trả về thành công mà không thực hiện hành động nghiệp vụ lần hai (ví dụ, không tạo hóa đơn thứ hai).
Replay là nút phục hồi của bạn. Đó là khả năng xử lý lại các sự kiện trong quá khứ có chủ đích, trong một phạm vi kiểm soát, sau khi sửa lỗi hoặc sau khi đối tác bị outage. Replay khác với retry: retry tự động và tức thời, replay là có chủ ý và thường xảy ra sau vài giờ hoặc vài ngày.
Nếu bạn muốn webhook đáng tin, hãy đặt vài mục tiêu đơn giản và thiết kế xung quanh chúng:
- Không mất sự kiện (bạn luôn có thể tìm thấy những gì đã đến hoặc bạn đã cố gửi)
- Trùng lặp an toàn (retry và replay không khiến tính phí/ tạo đôi/ gửi email đôi)
- Dấu vết audit rõ ràng (bạn có thể trả lời "chuyện gì đã xảy ra?" nhanh chóng)
Một cách thực tế để hỗ trợ cả ba là lưu mọi lần thử webhook với trạng thái và một khóa idempotency duy nhất. Nhiều team xây cái này như một bảng "webhook inbox/outbox" nhỏ.
Inbound webhooks: một luồng receiver bạn có thể tái dùng
Hầu hết vấn đề webhook xảy ra vì sender và receiver chạy trên hai đồng hồ khác nhau. Nghĩa vụ của bạn với tư cách receiver là phải dễ đoán: xác nhận nhanh, ghi lại những gì đến và xử lý an toàn.
Tách "chấp nhận" khỏi "thực thi"
Bắt đầu với một luồng giữ request HTTP nhanh và chuyển công việc thực sự sang nơi khác. Điều này giảm timeout và làm cho retry bớt đau đầu.
- Acknowledge nhanh. Trả về 2xx ngay khi request chấp nhận được.
- Kiểm tra cơ bản. Xác thực content type, các trường bắt buộc, và parsing. Nếu webhook được ký, xác minh chữ ký ở đây.
- Lưu payload thô. Lưu body cùng các header bạn cần sau này (signature, event ID), kèm timestamp nhận và trạng thái như "received".
- Đưa vào queue để xử lý. Tạo job xử lý nền, rồi trả 2xx.
- Xử lý với kết quả rõ ràng. Đánh dấu event là "processed" chỉ sau khi các side effect thành công. Nếu thất bại, ghi lại lý do và liệu có nên retry hay không.
"Phản hồi nhanh" trông như thế nào
Mục tiêu thực tế là phản hồi dưới 1 giây. Nếu sender mong một mã cụ thể, hãy dùng mã đó (nhiều bên chấp nhận 200, một số thích 202). Chỉ trả 4xx khi sender không nên retry (ví dụ: chữ ký không hợp lệ).
Ví dụ: một webhook "customer.created" đến khi cơ sở dữ liệu của bạn đang tải nặng. Với luồng trên, bạn vẫn lưu payload thô, đưa vào hàng đợi và trả 2xx. Worker sau đó có thể retry mà không cần sender gửi lại.
Kiểm tra an toàn inbound mà không làm gãy delivery
Kiểm tra bảo mật đáng để làm, nhưng mục tiêu là chặn traffic xấu mà không chặn sự kiện thật. Rất nhiều vấn đề giao nhận đến từ receiver quá nghiêm ngặt hoặc trả sai phản hồi.
Bắt đầu bằng cách chứng minh sender. Ưu tiên các request được ký (HMAC trong header) hoặc token chia sẻ trong header. Xác minh trước khi làm việc nặng, và fail nhanh nếu thiếu hoặc sai.
Cẩn trọng với mã trạng thái vì chúng điều khiển retry:
- Trả 401/403 cho lỗi xác thực để sender không retry vô hạn.
- Trả 400 cho JSON sai cú pháp hoặc thiếu trường bắt buộc.
- Trả 5xx chỉ khi dịch vụ của bạn tạm thời không thể nhận hoặc xử lý.
IP allowlist có thể hữu ích, nhưng chỉ khi nhà cung cấp có dải IP ổn định và có tài liệu. Nếu IP của họ thay đổi thường xuyên (hoặc họ dùng pool cloud lớn), allowlist có thể âm thầm làm rơi các webhook thật và bạn chỉ nhận ra muộn.
Nếu nhà cung cấp gửi timestamp và event ID, bạn có thể thêm bảo vệ chống replay: từ chối các thông điệp quá cũ, và theo dõi ID gần đây để phát hiện trùng lặp. Giữ cửa sổ thời gian nhỏ, nhưng để một khoảng lùi hợp lý để đồng hồ lệch không phá vỡ các request hợp lệ.
Danh sách kiểm tra bảo mật thân thiện với receiver:
- Xác thực chữ ký hoặc secret trước khi parse payload lớn.
- Đặt giới hạn kích thước body và timeout request ngắn.
- Dùng 401/403 cho lỗi auth, 400 cho JSON sai, và 2xx cho sự kiện được chấp nhận.
- Nếu kiểm tra timestamp, cho một cửa sổ lùi nhỏ (ví dụ vài phút).
Về logging, giữ dấu vết audit mà không lưu dữ liệu nhạy cảm mãi mãi. Lưu event ID, tên sender, thời gian nhận, kết quả xác minh và hash của body thô. Nếu phải lưu payload, đặt hạn lưu trữ và che các trường như email, token hoặc chi tiết thanh toán.
Retry nên giúp, không gây hại
Retry là tốt khi nó biến một trục trặc ngắn thành một giao nhận thành công. Chúng có hại khi nhân bản traffic, che lỗi thật, hoặc tạo trùng lặp. Sự khác biệt nằm ở quy tắc rõ ràng về khi nào retry, giãn cách thế nào và khi nào dừng.
Làm chuẩn: chỉ retry khi receiver có khả năng thành công sau. Mô hình tư duy hữu ích: retry cho các lỗi "tạm thời", không retry cho "bạn gửi sai".
Kết quả HTTP thực tế:
- Retry: timeout mạng, lỗi kết nối, và HTTP 408, 429, 500, 502, 503, 504
- Không retry: HTTP 400, 401, 403, 404, 422
- Tùy trường hợp: HTTP 409 (đôi khi là "đã trùng", đôi khi là conflict thật)
Cách giãn retry quan trọng. Dùng exponential backoff với jitter để tránh bão retry khi nhiều event thất bại cùng lúc. Ví dụ: chờ 5s, 15s, 45s, 2m, 5m, rồi thêm một khoảng ngẫu nhiên nhỏ mỗi lần.
Cũng hãy đặt giới hạn cửa sổ retry và ngưỡng dừng rõ ràng. Lựa chọn phổ biến là "thử trong tối đa 24 giờ" hoặc "không quá 10 lần thử". Sau đó, xem nó như một vấn đề phục hồi, không phải vấn đề giao nhận.
Để hoạt động hàng ngày, bản ghi sự kiện của bạn nên lưu:
- Số lần thử
- Lỗi cuối cùng
- Thời gian thử tiếp theo
- Trạng thái cuối (bao gồm dead-letter khi bạn ngừng retry)
Các mục dead-letter nên dễ inspect và an toàn để replay sau khi bạn sửa vấn đề gốc.
Các mẫu idempotency hiệu quả trong thực tế
Idempotency nghĩa là bạn có thể xử lý cùng một webhook nhiều lần mà không tạo thêm side effect. Đây là một trong những cách cải thiện độ tin cậy nhanh nhất, bởi vì retry và timeout sẽ xảy ra ngay cả khi không ai làm gì sai.
Chọn khóa ổn định
Nếu nhà cung cấp cho event ID, dùng nó. Đó là lựa chọn sạch nhất.
Nếu không có event ID, xây khóa từ các trường ổn định bạn có, ví dụ một hash của:
- tên nhà cung cấp + loại sự kiện + resource ID + timestamp, hoặc
- tên nhà cung cấp + message ID
Lưu khóa cùng một ít metadata (thời gian nhận, nhà cung cấp, loại sự kiện và kết quả xử lý).
Quy tắc thường bền vững:
- Xem khóa như bắt buộc. Nếu bạn không thể xây, cách ly sự kiện thay vì đoán.
- Lưu khóa với TTL (ví dụ 7–30 ngày) để bảng không phình mãi.
- Lưu cả kết quả xử lý (success, failed, ignored) để trùng lặp nhận được phản hồi nhất quán.
- Đặt ràng buộc unique trên khóa để hai request song song không đều chạy.
Làm cho hành động nghiệp vụ cũng idempotent
Ngay cả với bảng khóa tốt, thao tác thực tế phải an toàn. Ví dụ: webhook "create order" không nên tạo đơn thứ hai nếu lần đầu insert vào DB thành công nhưng client không nhận được phản hồi. Dùng các định danh nghiệp vụ tự nhiên (external_order_id, external_user_id) và pattern upsert.
Sự kiện sai thứ tự là chuyện thường. Nếu bạn nhận "user_updated" trước "user_created", đặt quy tắc như "chỉ áp dụng khi event_version mới hơn" hoặc "chỉ cập nhật nếu updated_at mới hơn dữ liệu đang có".
Trùng lặp với payload khác là trường hợp khó nhất. Quyết định trước cách xử lý:
- Nếu khóa trùng nhưng payload khác, coi đó là lỗi của nhà cung cấp và cảnh báo.
- Nếu khóa trùng và payload chỉ khác ở các trường không quan trọng, bỏ qua.
- Nếu bạn không thể tin nhà cung cấp, chuyển sang khóa dẫn xuất từ hash toàn bộ payload và xử lý xung đột như sự kiện mới.
Mục tiêu là đơn thay đổi thế giới thực tạo ra một kết quả thật sự duy nhất, ngay cả khi bạn thấy tin nhắn ba lần.
Công cụ replay và nhật ký audit cho phục hồi
Khi hệ thống đối tác chập chờn, độ tin cậy ít liên quan tới giao hàng hoàn hảo và nhiều hơn tới phục hồi nhanh. Công cụ replay biến "mất vài sự kiện" thành việc sửa chữa theo quy trình thay vì khủng hoảng.
Bắt đầu bằng nhật ký sự kiện theo lifecycle của từng webhook: received, processed, failed, hoặc ignored. Giữ khả năng tìm kiếm theo thời gian, loại sự kiện và correlation ID để support có thể trả lời nhanh "đơn 18432 đã thế nào?".
Với mỗi sự kiện, lưu đủ ngữ cảnh để chạy lại quyết định cùng một cách sau này:
- Payload thô và header khóa (signature, event ID, timestamp)
- Các trường đã chuẩn hóa bạn trích ra
- Kết quả xử lý và thông báo lỗi (nếu có)
- Phiên bản workflow hoặc mapping dùng lúc đó
- Timestamp cho nhận, bắt đầu, kết thúc
Khi có những thứ đó, thêm hành động "Replay" cho các sự kiện thất bại. Nút ít quan trọng hơn các rào chắn. Một luồng replay tốt hiển thị lỗi trước đó, sẽ xảy ra gì khi replay, và liệu sự kiện có an toàn để chạy lại không.
Rào chắn tránh gây hại vô tình:
- Yêu cầu ghi lý do trước khi replay
- Hạn chế quyền replay cho một vai trò nhỏ
- Chạy lại qua cùng các kiểm tra idempotency như lần đầu
- Giới hạn tốc độ replay để tránh gây spike mới trong sự cố
- Tùy chọn chế độ dry run để validate mà không ghi thay đổi
Sự cố thường liên quan nhiều sự kiện, nên hỗ trợ replay theo khoảng thời gian (ví dụ, "replay tất cả sự kiện thất bại giữa 10:05 và 10:40"). Ghi lại ai replay, khi nào và vì lý do gì.
Outbound webhooks: một luồng sender bạn có thể audit
Outbound webhook thất bại vì lý do tẻ nhạt: receiver chậm, outage thoáng qua, DNS hiccup, hoặc proxy làm rớt request dài. Độ tin cậy đến từ việc coi mỗi lần gửi như một job được theo dõi, không phải một cuộc gọi HTTP độc lập.
Luồng sender giữ ổn định
Gán cho mỗi sự kiện một event ID duy nhất và ổn định. ID đó nên giữ nguyên qua các retry, replay và cả khi service khởi động lại. Nếu bạn tạo ID mới cho mỗi lần thử, bạn làm khó việc dedupe cho receiver và audit cho chính bạn.
Tiếp theo, ký mỗi request và kèm timestamp. Timestamp giúp receiver từ chối các request rất cũ, và chữ ký chứng minh payload không bị thay đổi trên đường đi. Giữ quy tắc chữ ký đơn giản và nhất quán để partner dễ triển khai.
Theo dõi việc giao theo từng endpoint, không chỉ theo event. Nếu bạn gửi cùng một event đến ba khách hàng, mỗi đích cần lịch sử attempt và trạng thái cuối riêng.
Một luồng thực tế hầu hết các team có thể triển khai:
- Tạo bản ghi event với event ID, endpoint ID, hash payload và trạng thái ban đầu.
- Gửi HTTP request với chữ ký, timestamp và header khóa idempotency.
- Ghi lại mọi attempt (thời gian bắt đầu, kết thúc, HTTP status, lỗi ngắn).
- Chỉ retry trên timeout và phản hồi 5xx, dùng exponential backoff với jitter.
- Dừng sau giới hạn rõ ràng (số lần tối đa hoặc tuổi tối đa), rồi đánh dấu failed để review.
Header khóa idempotency quan trọng ngay cả khi bạn là sender. Nó cho receiver một cách sạch để dedupe nếu họ đã xử lý lần đầu nhưng client của bạn không nhận được 200.
Cuối cùng, làm cho lỗi hiển thị. "Failed" không nên nghĩa là "mất". Nó nên nghĩa là "tạm dừng với đủ ngữ cảnh để phát lại an toàn".
Ví dụ: hệ thống đối tác chập chờn và phục hồi sạch
Ứng dụng support của bạn gửi cập nhật ticket đến một hệ thống đối tác để agent họ thấy cùng trạng thái. Mỗi khi ticket thay đổi (giao cho ai, cập nhật priority, đóng), bạn post một webhook như ticket.updated.
Một buổi chiều endpoint của partner bắt đầu timeout. Lần gửi đầu tiên của bạn chờ, chạm timeout của client và bạn coi là "không rõ" (có thể họ đã nhận, có thể không). Một chiến lược retry tốt sẽ retry theo backoff thay vì gửi lại mỗi giây. Sự kiện nằm trong hàng đợi với cùng event ID, và mỗi lần thử được ghi lại.
Bây giờ phần đau đầu: nếu bạn không dùng idempotency, partner có thể xử lý trùng lặp. Attempt #1 có thể đã tới họ nhưng phản hồi không về. Attempt #2 tới sau và tạo một hành động "Ticket closed" nữa, gửi hai email hoặc tạo hai mục timeline.
Với idempotency, mỗi lần gửi kèm khóa idempotency được dẫn xuất từ event (thường là event ID). Partner lưu khóa đó trong một khoảng thời gian và trả về "already processed" cho các lần lặp. Bạn không còn phải đoán.
Khi partner khôi phục, replay là cách bạn sửa cập nhật thực sự bị mất (ví dụ thay đổi priority trong outage). Bạn chọn sự kiện từ audit log và replay nó một lần, cùng payload và khóa idempotency, nên an toàn ngay cả khi họ đã nhận trước đó.
Trong sự cố, log của bạn nên kể lại câu chuyện rõ ràng:
- Event ID, ticket ID, loại sự kiện và phiên bản payload
- Số attempt, timestamp và thời gian thử tiếp theo
- Timeout vs non-2xx response vs success
- Khóa idempotency được gửi, và partner có báo "duplicate" không
- Bản ghi replay cho biết ai replay và kết quả cuối cùng
Những sai lầm và bẫy thường gặp
Hầu hết incident webhook không phải do một bug lớn. Chúng xuất phát từ các quyết định nhỏ dần dần phá vỡ độ tin cậy khi traffic tăng hoặc bên thứ ba chập chờn.
Các bẫy thường xuất hiện trong postmortem:
- Làm việc chậm trong handler request (ghi DB, gọi API, upload file) đến khi sender timeout và retry
- Giả sử nhà cung cấp không bao giờ gửi trùng, rồi charge đôi, tạo đơn đôi hoặc gửi hai email
- Trả sai mã trạng thái (200 ngay cả khi bạn không chấp nhận sự kiện, hoặc 500 cho dữ liệu sai mà không bao giờ thành công khi retry)
- Triển khai không có correlation ID, event ID hoặc request ID, rồi mất hàng giờ để đối chiếu log với báo cáo khách hàng
- Retry mãi mãi, gây backlog và biến outage của partner thành outage của bạn
Một quy tắc đơn giản bền vững: acknowledge nhanh, rồi xử lý an toàn. Chỉ validate đủ để quyết accept sự kiện, lưu nó, rồi làm phần còn lại bất đồng bộ.
Mã trạng thái quan trọng hơn nhiều người nghĩ:
- Dùng 2xx chỉ khi bạn đã lưu sự kiện (hoặc đưa vào queue) và tin rằng nó sẽ được xử lý.
- Dùng 4xx cho input không hợp lệ hoặc auth thất bại để sender ngừng retry.
- Dùng 5xx chỉ cho vấn đề tạm thời phía bạn.
Đặt trần retry. Dừng sau một cửa sổ cố định (như 24 giờ) hoặc số lần cố định, rồi đánh dấu sự kiện "needs review" để con người quyết định replay gì.
Checklist nhanh và bước tiếp theo
Độ tin cậy webhook chủ yếu là những thói quen có thể lặp: accept nhanh, dedupe mạnh mẽ, retry cẩn trọng và giữ đường phát lại.
Kiểm tra nhanh inbound (receiver)
- Trả 2xx nhanh sau khi request được lưu an toàn (làm việc chậm bất đồng bộ).
- Lưu đủ thông tin của sự kiện để chứng minh bạn đã nhận (và debug sau này).
- Yêu cầu khóa idempotency (hoặc dẫn xuất từ provider + event ID) và bắt buộc trong DB.
- Dùng 4xx cho chữ ký sai hoặc schema không hợp lệ, và 5xx chỉ cho vấn đề server thực sự.
- Theo dõi trạng thái xử lý (received, processed, failed) kèm lỗi cuối.
Kiểm tra nhanh outbound (sender)
- Gán event ID duy nhất cho mỗi sự kiện và giữ nó ổn định qua các lần thử.
- Ký mọi request và kèm timestamp.
- Định nghĩa chính sách retry (backoff, số lần tối đa và khi nào dừng) và tuân thủ.
- Theo dõi trạng thái theo endpoint: last success, last failure, consecutive failures, next retry time.
- Ghi log mọi attempt với đủ chi tiết cho support và audit.
Với ops, quyết trước những gì bạn sẽ replay (sự kiện đơn, batch theo thời gian/trạng thái, hoặc cả hai), ai được phép làm và quy trình review dead-letter.
Nếu bạn muốn xây những phần này mà không phải nối mọi thứ bằng tay, một nền tảng no-code như AppMaster (appmaster.io) có thể phù hợp: bạn có thể mô hình bảng webhook inbox/outbox trên PostgreSQL, triển khai luồng retry và replay trong Business Process Editor bằng giao diện trực quan, và cung cấp panel admin nội bộ để tìm và chạy lại sự kiện khi đối tác chập chờn.
Câu hỏi thường gặp
Webhooks nằm giữa các hệ thống mà bạn không kiểm soát, nên bạn thừa hưởng timeout, outage, retry và thay đổi schema từ họ. Ngay cả khi mã của bạn đúng, bạn vẫn có thể gặp trùng lặp, mất sự kiện, trễ hoặc thứ tự gửi sai.
Thiết kế từ đầu cho việc thử lại và trùng lặp. Lưu mọi sự kiện đến, trả về 2xx nhanh sau khi đã ghi nhận an toàn, và xử lý bất đồng bộ với một khóa idempotency để các lần gửi lặp lại không gây lặp hành động nghiệp vụ.
Bạn nên xác nhận nhanh sau khi đã kiểm tra cơ bản và lưu trữ, thường là dưới 1 giây. Nếu bạn làm việc chậm trong handler, người gửi sẽ timeout và thử lại, điều này làm tăng trùng lặp và khiến sự cố khó gỡ hơn.
Tính idempotency là “thực hiện hành động nghiệp vụ một lần, ngay cả khi tin nhắn đến nhiều lần.” Bạn thực thi bằng cách dùng khóa idempotency ổn định (thường là event ID của nhà cung cấp), lưu nó lại, và trả về thành công cho các bản trùng mà không chạy lại hành động.
Nếu nhà cung cấp có event ID thì dùng nó. Nếu không, dẫn xuất khóa từ các trường ổn định mà bạn tin tưởng, và tránh các trường có thể thay đổi giữa các lần thử. Nếu không thể xây khóa ổn định, cách an toàn là đưa sự kiện vào khu vực cách ly để review thay vì đoán mò.
Trả 4xx cho các lỗi mà người gửi không thể sửa bằng cách thử lại, ví dụ như xác thực thất bại hoặc payload bị sai. Dùng 5xx chỉ cho các vấn đề tạm thời phía bạn. Hãy nhất quán vì mã trạng thái thường quyết định liệu người gửi có thử lại hay không.
Thử lại khi gặp timeout, lỗi kết nối và các phản hồi tạm thời như 408, 429 và 5xx. Dùng exponential backoff với jitter và có ngưỡng dừng rõ ràng, ví dụ số lần tối đa hoặc tuổi tối đa của sự kiện; sau đó chuyển sự kiện sang trạng thái cần review.
Replay là việc chủ động xử lý lại sự kiện trong quá khứ sau khi bạn sửa lỗi hoặc phục hồi. Retry thì tự động và xảy ra ngay lập tức. Replay tốt cần nhật ký sự kiện, kiểm tra idempotency an toàn và các rào chắn để tránh nhân đôi công việc vô ý.
Giả sử bạn sẽ nhận sự kiện sai thứ tự và đặt ra quy tắc phù hợp với nghiệp vụ. Cách phổ biến là chỉ áp dụng khi event version hoặc timestamp mới hơn so với dữ liệu hiện có, để các sự kiện đến muộn không ghi đè trạng thái hiện tại.
Xây một bảng inbox/outbox đơn giản và một view admin để tìm kiếm, kiểm tra và phát lại các sự kiện thất bại. Trong AppMaster (appmaster.io), bạn có thể mô hình các bảng này trên PostgreSQL, triển khai luồng dedupe, retry và replay trong Business Process Editor, và xuất một panel nội bộ cho support mà không phải code toàn bộ từ đầu.


