Database của bạn ngừng hoạt động lúc 2 giờ sáng. Đến lúc bạn thức dậy và sửa xong, ba giờ webhook đã được gửi tới và bị trả lỗi. Giờ bạn đang nhìn chằm chằm vào Slack, tự hỏi bao nhiêu khoản thanh toán Stripe chưa được xử lý đơn hàng và bao nhiêu lần push lên GitHub đã không kích hoạt đúng job CI.
Bài này là quy trình thực tế để khôi phục các webhook bị lỡ, theo từng nhà cung cấp, rồi đến mô hình biến chuyện này thành vấn đề của một cú bấm thay vì cả một buổi chiều Chủ nhật.
TL;DR
- Stripe: gửi lại thủ công từng sự kiện từ dashboard, hoặc viết script qua Events API. Không có giao diện gửi lại hàng loạt.
- GitHub: một nút gửi lại cho từng lượt gửi. Cũng là từng cái một, trừ khi bạn viết script gọi Redelivery API.
- Shopify: thử lại (retry) 8 lần trong 4 giờ, sau đó sự kiện mất và bạn phải truy vấn lại Orders API để đối soát.
- Phát lại (replay) bằng một cú bấm đòi hỏi ghi log ở upstream trước khi gửi, và đó chính là toàn bộ thiết kế của một dịch vụ trung chuyển webhook (webhook relay).
Stripe
Stripe cho phép bạn gửi lại sự kiện webhook thủ công từ dashboard:
- Vào Developers → Events trong Stripe Dashboard
- Bấm vào sự kiện bạn muốn gửi lại
- Cuộn tới Webhook attempts và bấm Resend (gửi lại)
Cách này chỉ làm được từng sự kiện một, và chỉ trong 15 ngày kể từ khi sự kiện được tạo. Nếu bạn lỡ 300 sự kiện, bạn sẽ phải bấm 300 nút. Có một lệnh Stripe CLI (stripe events resend <event_id>) nhưng nó vẫn yêu cầu bạn biết ID của từng sự kiện.
Phát lại hàng loạt sự kiện Stripe
Không có giao diện phát lại hàng loạt chính chủ. Các cách xoay xở:
- Viết một script dùng Stripe API để liệt kê các sự kiện trong khoảng thời gian sự cố, lọc theo trạng thái, rồi tự POST từng sự kiện về endpoint của bạn. Bạn đang dựng lại webhook từ object sự kiện, và nó không hoàn toàn cùng cấu trúc.
- Hoặc, nếu bạn đã ghi log webhook ở upstream, phía trước chỗ xảy ra sự cố, phát lại từ log của chính bạn
Cách ghi log ở upstream là thứ mà các team chạy production rốt cuộc đều tự xây. Đó cũng chính xác là điều một webhook relay làm cho bạn.
GitHub
GitHub có nút gửi lại cho từng lượt gửi trong Settings → Webhooks → (chọn webhook) → Recent Deliveries của repo. Bấm Redeliver cạnh từng lượt.
Cùng vấn đề: từng cái một. GitHub chỉ cho gửi lại các lượt gửi trong 3 ngày, và phát lại hàng loạt vẫn đồng nghĩa với một script gọi tới Redelivery API. Và log đó là tất cả những gì bạn có, vì GitHub không bao giờ tự thử lại một lượt gửi thất bại.
Shopify
Shopify thử lại 8 lần trong khoảng bốn giờ rồi bỏ cuộc. Sau đó, cách duy nhất để khôi phục là truy vấn lại Shopify API để lấy những gì bạn đã lỡ. Bản thân các webhook đã mất.
Trường hợp này đặc biệt đau vì thời hạn khôi phục của Shopify ngắn nhất, bỏ xa hai ông lớn còn lại. Nếu server của bạn ngừng hoạt động tối thứ Sáu và đến thứ Hai bạn mới phát hiện, bạn sẽ phải đối soát toàn bộ từ Orders API.
Quy trình chung
Dù là nhà cung cấp nào, quy trình khôi phục khi không có relay trông như sau:
- Xác định khoảng thời gian sự cố. Server của bạn ngừng trả 2xx từ lúc nào?
- Lấy các sự kiện bị ảnh hưởng, thường là một lệnh gọi API có phân trang cho mỗi nhà cung cấp
- Bắn lại từng sự kiện bằng cách POST nó tới endpoint của chính bạn, hoặc truy vấn lại nguồn dữ liệu gốc và đồng bộ trạng thái thủ công
- Chống trùng lặp, vì handler (đoạn code xử lý webhook trên server của bạn) phải idempotent (xử lý nhiều lần vẫn cho cùng một kết quả) và có thể một phần sự kiện đã đến được rồi
Việc này mất hàng giờ. Và bạn chỉ có một cơ hội: nếu bạn lỡ các sự kiện quá khoảng thời gian thử lại, chúng mất hẳn.
Phiên bản một cú bấm
AnyHook tồn tại để biến tất cả những việc này thành một cú bấm. Khi bạn trỏ webhook tới in.anyhook.net/you/app:
- Mọi sự kiện được lưu trước khi gửi, mã hóa khi lưu trữ
- Nếu server của bạn trả 5xx, AnyHook thử lại theo exponential backoff, tức khoảng chờ tăng theo cấp số nhân (Free=3, Pro=5, Scale=10 lần gửi)
- Nếu server của bạn ngừng hoạt động đủ lâu để hết lượt thử lại, sự kiện bị đánh dấu thất bại, nhưng payload vẫn nằm trong nhật ký của bạn
- Mở trang của App (một URL nhận cho một nguồn webhook, ví dụ “Stripe production”), chọn khoảng thời gian trong banner sự kiện thất bại (một giờ qua, 24 giờ qua hoặc toàn bộ thời hạn phát lại), bấm Replay. Mỗi lần nó gửi 100 sự kiện, cũ nhất trước. Vậy thôi.
Phát lại sự kiện thất bại không bị tính vào hạn mức hằng tháng của bạn, nhưng vẫn dùng đến hạn mức compute (tài nguyên tính toán đi kèm gói). Mỗi gói có giới hạn phát lại hằng ngày (100 với gói Free, 500 với gói Pro, 1.000 với gói Scale), và một sự kiện được tạm giữ vì vượt hạn mức sẽ được tính là sự kiện mới khi được phát lại. Lượt phát lại được gửi mới tới endpoint của bạn với chữ ký mới, trong khi body và header gốc lúc nhận được giữ nguyên, nên handler của bạn thấy sự kiện đúng như nhà cung cấp đã gửi.
Cách tương đương qua API là một lệnh POST duy nhất:
curl -X POST https://anyhook.net/api/v1/apps/stripe-prod/replay-failed \
-H "Authorization: Bearer $ANYHOOK_KEY" \
-H "Content-Type: application/json" \
-d '{"since": "2026-04-18T02:00:00Z", "limit": 100}'
Một lệnh gọi, phát lại tối đa 100 sự kiện, không cần bấm gì trên dashboard.
Hệ quả về thiết kế
Thứ làm cho phát lại trở nên rẻ là lưu trữ tại edge, trước khi gửi. Nếu nhật ký webhook của bạn bắt đầu ở origin, bạn chỉ phát lại được những gì origin đã nhận. Nếu nhật ký bắt đầu ở edge của mạng, trước khi origin có cơ hội hỏng, bạn có thể phát lại bất cứ thứ gì từng đến.
Lịch chạy giữa các lần gửi đó là một bài toán thiết kế riêng. Lịch của AnyHook là một thang cố định, không có jitter, và bài Exponential backoff và jitter cho việc thử lại webhook giải thích vì sao một thang như vậy dồn mọi client vào cùng một giây sau sự cố, và vì sao dù thế nào thì phát lại dễ dàng vẫn tốt hơn thử lại dồn dập. Và nếu bạn thấy mình thường xuyên phải dùng đến phát lại, câu hỏi cần đặt ra sớm hơn là ranh giới này có thực sự cần webhook hay không, chủ đề của bài Webhook và polling.
Chuyển việc ghi log lên upstream, làm cho phát lại idempotent, và sự cố webhook tồi tệ nhất của bạn sẽ chỉ là một lần khôi phục trong một phút thay vì cả một cuối tuần.
Hai bài tiếp theo đáng đọc trước khi bạn cần đến. “Làm cho phát lại idempotent” là nửa gánh phần lớn sức nặng, và nó khó hơn một bảng chống trùng lặp khi các lần thử lại đến đồng thời: Bạn nhận cùng một webhook hai lần. Và nếu Stripe đã bỏ cuộc và vô hiệu hóa endpoint của bạn, nó đã ngừng thử lại và bạn đang phải lấy bù (backfill) từ Events API trong thời hạn 30 ngày: Stripe đã vô hiệu hóa endpoint webhook của bạn.
Dùng thử: anyhook.net.