Webhook18 tháng 4, 20268 phút đọc

Stripe webhook retry: hướng dẫn cho môi trường production

Stripe thử lại webhook thất bại trong tối đa 3 ngày. Lịch thử lại hoạt động thế nào, cái gì hỏng khi endpoint ngừng hoạt động và cách thiết kế cho nó.

KHOẢNG CÁCH GIỮA CÁC LẦN THỬ · TĂNG THEO CẤP SỐ NHÂNbây giờ5 phút30 phút3 giờ18 giờbỏ cuộcstatus=failed · payload vẫn nằm trong nhật ký · vẫn phát lại được
Ba ngày thử lại, rồi im lặng. Nhật ký của chính bạn quyết định sự im lặng đó có đáng lo hay không.

Khi server của bạn ngừng hoạt động và Stripe bắn đi một payment_intent.succeeded, chuyện gì xảy ra với sự kiện đó? Stripe có thử lại (retry) mãi không? Trong bao lâu? Và bạn đối soát những sự kiện đã bỏ lỡ bằng cách nào?

Đây là câu hỏi tôi nghe nhiều nhất từ các developer tích hợp Stripe, và nó quan trọng vì một webhook bạn không xử lý được là doanh thu mà sản phẩm của bạn không thể phản hồi. Khách hàng vừa thanh toán. Job xử lý đơn hàng của bạn không bao giờ chạy. Dashboard của họ không bao giờ cập nhật.

TL;DR

  • Stripe thử lại trong 3 ngày theo exponential backoff (khoảng chờ tăng theo cấp số nhân), rồi dừng hẳn
  • Trả về 200 nhanh, xử lý bất đồng bộ, ghi lại mọi sự kiện trước khi xử lý và bảo đảm idempotent (xử lý nhiều lần vẫn cho cùng một kết quả)
  • Nếu bạn không có thời gian xây tất cả những thứ này, một dịch vụ trung chuyển webhook (webhook relay) cho bạn thử lại, nhật ký và phát lại (replay) chỉ bằng việc đổi một URL

Stripe thử lại như thế nào

Stripe mong endpoint webhook của bạn trả về mã trạng thái 2xx thật nhanh. Stripe không công bố con số chính xác, và tài liệu của họ chỉ nói trả về 2xx “trước mọi logic phức tạp có thể gây timeout”. Con số cộng đồng vẫn dựa vào là 20 giây. Một lượt gửi bị timeout được tính là thất bại, cũng như các phản hồi 5xx và lỗi mạng.

Khi thất bại, Stripe thử lại sự kiện theo exponential backoff trong tối đa 3 ngày. Các lần thử lại ngày càng thưa dần, và lần cuối rơi vào khoảng 72 giờ tính từ sự kiện gốc.

Sau 3 ngày, Stripe đánh dấu sự kiện là thất bại và ngừng thử lại. Stripe không đánh thức bạn dậy. Không gửi email dự phòng nào. Sự kiện đơn giản là biến mất khỏi hàng đợi gửi của bạn, dù bạn vẫn xem được nó trong Stripe Dashboard ở mục Developers → Events.

Đường cong của Stripe cũng thuộc loại nhẹ nhàng. Các nền tảng merchant-of-record có thể thử lại dồn dập hơn nhiều: Paddle gửi tối đa 60 lần trong khoảng hai ngày, và một handler (đoạn code xử lý webhook trên server của bạn) theo kịp lịch của Stripe vẫn có thể bị lịch của Paddle nhấn chìm.

Nếu lỗi tiếp diễn quá mức đó, cuối cùng Stripe sẽ vô hiệu hóa hẳn endpoint và gửi email báo ngày ngắt. Khôi phục từ tình huống đó là một việc khác, và các sự kiện vẫn lấy về được từ Events API trong 30 ngày: xem bài Stripe đã vô hiệu hóa endpoint webhook của bạn để biết trình tự lấy bù (backfill).

Ba kiểu hỏng gây đau đầu

Endpoint của bạn chậm. Stripe tính một phản hồi vượt quá mức timeout không công bố của họ là thất bại. Nếu handler webhook ghi đồng bộ vào một database chậm, đồng bộ sang Salesforce và gửi email cho khách hàng, bạn đang đùa với timeout ở mọi sự kiện.

Khung giờ deploy. Server của bạn khởi động lại trong lúc deploy, bỏ lỡ khoảng 10 sự kiện, rồi chạy lại. Stripe thử lại những sự kiện đó, nhưng chỉ trong 3 ngày.

Một bug sinh ra hàng loạt lỗi 500. Bạn đẩy lên một bản deploy ném lỗi với một loại sự kiện cụ thể. Stripe thử lại nhưng cứ gặp đúng lỗi đó. Sau 3 ngày Stripe bỏ cuộc và bạn có một khoảng trống dữ liệu lặng lẽ.

Trong cả ba trường hợp, Stripe đều làm đúng. Lỗi nằm ở phía đường ống của bạn.

Bốn điều mà tích hợp đạt chuẩn production đều làm

1. Trả về 200 nhanh, xử lý bất đồng bộ

Đừng làm việc nặng trong handler webhook. Nhận sự kiện, ghi nó vào một hàng đợi hoặc nhật ký bền vững, trả về 200, rồi xử lý bất đồng bộ. Cách này giữ bạn ở xa ngưỡng timeout và tách lượt gửi khỏi business logic.

export async function POST(req: Request) {
  const event = await verifyStripeSignature(req);
  await queue.publish(event);   // durable write
  return new Response(null, { status: 200 });
}
waitUntil · MÃ 200 LÀ LỜI HỨA BẠN KHÔNG GIỮ ĐƯỢCxác minhtrả về 200waitUntil(work)chỉ nằm trong bộ nhớbị thu hồi → mất, không gửi lạiLƯU TRƯỚC · MÃ 200 LÀ THẬTxác minhghi bền vữngtrả về 200xử lýthử lại được
Trả về 200 cho thứ bạn đã ghi lại bền vững, và đồng hồ của nhà cung cấp không còn ràng buộc business logic của bạn nữa.

2. Ghi lại mọi sự kiện trước khi xử lý

Lưu raw payload và header trước khi đụng tới business logic. Nếu logic ném lỗi, bạn có thể phát lại từ nhật ký. Stripe ngừng thử lại sau 3 ngày và giữ sự kiện trong 30 ngày, nên quá mốc đó, nhật ký của bạn là thứ duy nhất giúp bạn khôi phục.

3. Idempotency ở phía bạn

Stripe gửi cùng một event.id ở các lần thử lại, nên handler của bạn phải idempotent: nếu bạn đã xử lý evt_1H... rồi, đừng xử lý lại. Một khuôn mẫu đơn giản:

INSERT INTO processed_events (stripe_event_id, processed_at)
VALUES ($1, NOW())
ON CONFLICT (stripe_event_id) DO NOTHING;

Nếu lệnh insert trả về 0 dòng bị ảnh hưởng, bạn đã thấy sự kiện này rồi. Bỏ qua.

Idempotency sâu hơn vẻ ngoài của nó khi các lần thử lại bắt đầu tới đồng thời, hoặc sai thứ tự. Bài Bạn nhận cùng một webhook hai lần nói về ranh giới transaction, khuôn mẫu claim-then-commit cho những side effect bạn không rollback được và vì sao version guard, chứ không phải chống trùng lặp, mới là thứ sửa được việc gửi sai thứ tự.

4. Cảnh báo khi thất bại

Nếu 5 sự kiện liên tiếp thất bại, bạn muốn biết trong vòng vài phút, chứ không phải khi khách hàng email cho bộ phận hỗ trợ. Theo dõi số lần thất bại liên tiếp và tự gửi cảnh báo cho chính mình.

AnyHook giúp được gì

AnyHook đứng trước server của bạn và giải quyết chuyện này ở tầng truyền tải. Bạn trỏ Stripe về in.anyhook.net/you/stripe thay vì origin của bạn, và chúng tôi:

  • Trả về 200 cho Stripe sau khoảng một giây, nên thời hạn phản hồi không bao giờ là vấn đề
  • Ghi lại mọi sự kiện với đầy đủ header và body, mã hóa khi lưu trữ
  • Thử lại lượt gửi tới server của bạn ở mốc 5 phút, 30 phút, 3 giờ, 18 giờ, sau đó hằng ngày: 3 lần gửi trên gói Free, 5 lần trên gói Pro, 10 lần trên gói Scale. Lịch của chính Stripe kéo dài tới ba ngày, nên trên gói Free và Pro, lưới an toàn dài hạn là nhật ký đã lưu và phát lại, không phải số lần thử lại
  • Cho bạn phát lại bằng một cú bấm. Nếu server của bạn ngừng hoạt động trong một giờ, trang App (một URL nhận cho một nguồn webhook, ví dụ “Stripe production”) phát lại các sự kiện thất bại của giờ đó chỉ bằng một lần bấm (tối đa 100 sự kiện mỗi lần), và 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, dù vẫn có giới hạn hằng ngày và dùng đến hạn mức compute (tài nguyên tính toán đi kèm gói)
  • Cảnh báo khi thất bại liên tiếp. Chúng tôi gửi email cho bạn ở mốc 1 và 5 lần thất bại liên tiếp. Ở mốc 20, chúng tôi tạm dừng gửi tới đích đó (lúc đầu 10 phút, lâu hơn nếu ngắt lặp lại) và gửi email lần nữa; sự kiện tới trong lúc tạm dừng được ghi lại để phát lại thay vì được gửi đi

Không SDK, chỉ đổi một URL. Lần gửi đầu tiên vẫn qua được bước kiểm tra Stripe hiện có của bạn. Các lần thử lại và phát lại mang dấu thời gian (timestamp) gốc của Stripe, thứ mà Stripe SDK từ chối sau năm phút, nên hãy kiểm tra header AnyHook-Signature để chấp nhận chúng.

Để xem thử trước khi đăng ký bất cứ thứ gì, một lệnh duy nhất trả về một URL nhận (inbound URL) đang hoạt động, không cần tài khoản:

curl -X POST https://anyhook.net/api/v1/quickstart

Phản hồi có một inbound_url để trỏ nhà cung cấp tới, một api_key để đọc nhật ký sự kiện qua API và một claim_url. Endpoint này là tạm thời: nó và các sự kiện của nó bị xóa sau 7 ngày, trừ khi bạn mở claim URL và đăng nhập để nhận về tài khoản, khi đó URL vẫn giữ nguyên.

Điều cần nhớ

Nếu hôm nay bạn đang mất sự kiện Stripe, cách khắc phục thường không phải là thử lại nhiều hơn, mà là lưu trước khi xử lý, để bạn luôn có thứ để phát lại.

Tất cả bài viết18 tháng 4, 2026 · 8 phút

Đừng để mất webhook nữa.

Đổi một URL. Có ngay cơ chế thử lại, nhật ký sự kiện và phát lại bằng một cú bấm. Hoặc xem tận mắt trước: đăng ký, chạy bài diễn tập sự cố 30 giây và xem một sự kiện vẫn còn nguyên khi server của bạn ngừng hoạt động. Không cần có server.