Kiến trúc3 tháng 8, 20268 phút đọc

Webhook hay polling: chọn theo kiểu hỏng, không theo mốt

Polling tốn request nhưng hỏng nhẹ nhàng. Webhook hiệu quả nhưng hỏng trong im lặng. Kèm mô hình lai mà phần lớn các tích hợp nghiêm túc rốt cuộc đều xây.

POLLING · KHOẢNG TRỐNG TỰ ĐƯỢC LẤPvòng lặpngừng 3 giờlần poll kế tiếpsince=cursorđã bắt kịpphục hồi không cần thêm dòng code nào bạn chưa viết sẵnWEBHOOK · KHOẢNG TRỐNG KHÔNG AI THẤYnhà cung cấp500s 3 giờthử lại, rồi thôikhông có gìkhông lỗi, không cursorkhông có tín hiệuở bất kỳ đâukhông có dữ liệu trông y hệt như không có hoạt động
Điều đáng so sánh không phải số request mỗi giờ, mà là thứ mỗi mô hình để lại khi nó hỏng.

Cách nhìn thường gặp là polling (định kỳ gọi API để hỏi dữ liệu mới) là cách làm ngây thơ, còn webhook là thứ bạn chuyển sang khi đã lên trình. Câu chuyện nghe gọn gàng, và nó đưa các team tới kiến trúc sai thường xuyên đến bất ngờ.

Câu hỏi hữu ích không phải là cách nào hiệu quả hơn. Webhook hiệu quả hơn, và bỏ xa. Câu hỏi hữu ích là mỗi cách làm gì khi hỏng, vì đó là đặc tính bạn sẽ phải sống chung lúc 3 giờ sáng, và hai cách gần như trái ngược hoàn toàn.

TL;DR

  • Polling hỏng ồn ào và khôi phục được: bạn nhận ra, và lần poll kế tiếp sẽ bắt kịp
  • Webhook hỏng im lặng và vĩnh viễn: không có lỗi nào, sự kiện cứ thế biến mất
  • Webhook buộc bạn xây phần hạ tầng mà polling cho không: thử lại (retry), chống trùng lặp, thứ tự và xác minh chữ ký
  • Phần lớn tích hợp trên production rốt cuộc đều là mô hình lai: webhook lo độ trễ, một vòng poll đối soát lo tính đúng đắn
  • Nếu chỉ xây được một, và tính đúng đắn quan trọng hơn độ trễ, hãy chọn polling

Bảng so sánh thường thấy

PollingWebhook
Độ trễBằng chu kỳ poll, trung bình bằng một nửaGần thời gian thực
Request lãng phíPhần lớnKhông có
Ai chịu chi phíBạnBên gửi
Cần endpoint công khaiKhôngCó
Thứ tựBạn kiểm soátKhông được đảm bảo
Thiết lậpMột lệnh gọi HTTP và một vòng lặpEndpoint, TLS, xác minh chữ ký, chống trùng lặp, thử lại

Đọc bảng đó thì webhook thắng ở những hàng người ta hay tối ưu. Đó là lý do rất nhiều team chuyển sang, đưa lên production, rồi lặng lẽ mất sự kiện suốt hai tháng.

Phép so sánh thực sự quyết định

Thay vào đó hãy hỏi: khi cái này hỏng, làm sao tôi biết?

Polling xuống cấp theo những cách tự lên tiếng. Server của bạn ngừng hoạt động, vòng lặp không chạy. Khi server chạy lại, lần poll kế tiếp hỏi “có gì thay đổi kể từ cursor cuối cùng của tôi” và nhận về tất cả. Khôi phục diễn ra tự động và không cần dòng code nào bạn chưa viết sẵn. Nếu API ngừng hoạt động, lần poll trả về 500 và bạn thấy ngay trong tỷ lệ lỗi của chính mình. Mô hình kéo (pull) nghĩa là bạn luôn biết vị trí tốt cuối cùng của mình, và riêng đặc tính đó đã đáng giá rất nhiều.

Webhook hỏng theo những cách vô hình từ phía bạn. Endpoint của bạn trả 500 suốt sáu giờ trong một lần deploy lỗi. Nhà cung cấp thử lại, bỏ cuộc, rồi đi tiếp. Log của bạn không có gì bất thường, vì bạn không thể ghi log một request mà bạn chưa bao giờ nhận. Không có cursor, không có bộ phát hiện khoảng trống, không có con số “tôi đang chậm 400 sự kiện” ở bất kỳ đâu trong hệ thống. Không có dữ liệu trông y hệt không có hoạt động, và vào một Chủ nhật yên ắng, bạn không thể phân biệt hai điều đó.

POLLING · KHOẢNG TRỐNG TỰ ĐƯỢC LẤPvòng lặpngừng 3 giờlần poll kế tiếpsince=cursorđã bắt kịpphục hồi không cần thêm dòng code nào bạn chưa viết sẵnWEBHOOK · KHOẢNG TRỐNG KHÔNG AI THẤYnhà cung cấp500s 3 giờthử lại, rồi thôikhông có gìkhông lỗi, không cursorkhông có tín hiệuở bất kỳ đâukhông có dữ liệu trông y hệt như không có hoạt động

Điểm bất đối xứng đó nên là thứ quyết định. Trường hợp xấu nhất của polling là dữ liệu cũ mà bạn sẽ bắt kịp. Trường hợp xấu nhất của webhook là dữ liệu bị thiếu mà bạn sẽ không bao giờ biết.

Các chi phí bậc hai theo đó mà ra. Với webhook, giờ bạn phải tự lo logic thử lại, vì thời gian thử lại của nhà cung cấp có hạn và ngay cả nhà cung cấp hào phóng cũng bỏ cuộc sau ba ngày. Bạn tự lo idempotency (xử lý nhiều lần vẫn cho cùng một kết quả), vì gửi theo kiểu ít nhất một lần (at-least-once) khiến bản trùng là điều chắc chắn xảy ra. Bạn tự lo phần chặn sai thứ tự, vì các lần thử lại có thể để sự kiện #2 vượt lên trước sự kiện #1. Bạn tự lo xác minh chữ ký, vì endpoint của bạn công khai và ai cũng POST tới được. Và bạn phải có một endpoint HTTPS công khai luôn hoạt động, một yêu cầu về độ sẵn sàng khác hẳn với “app của chúng tôi thường thì vẫn chạy”.

Riêng từng thứ thì không khó. Gộp lại, chúng là lý do “cứ dùng webhook đi” biến thành một dự án hai tuần mà vẫn chưa xong khi bạn tưởng là xong.

Khi nào polling thực sự là câu trả lời đúng

Đừng để lập luận về hiệu quả thuyết phục bạn bỏ qua những trường hợp này.

Trường hợp hiển nhiên là độ trễ bạn không cần. Một lượt đồng bộ kế toán hằng đêm, một lượt đối soát tồn kho, một job làm giàu dữ liệu CRM: nếu không ai để ý độ trễ 5 phút, bạn đang trả giá bằng độ phức tạp thật cho một đặc tính chẳng có giá trị với ai.

Rồi đến trường hợp không có chỗ nào để nhận. App desktop, công cụ nội bộ sau VPN, một job chạy trên laptop của ai đó. Dùng tunnel đưa webhook vào localhost là kỹ thuật cho lúc phát triển chứ không phải kiến trúc production, và chúng tôi đã viết về ranh giới đó trong bài Vì sao ngrok là chưa đủ.

Trường hợp các team hay sai nhất là khi tính đúng đắn quan trọng hơn độ trễ và hạn chót chỉ còn một tuần. Một vòng poll dựa trên cursor mà bạn hiểu rõ vẫn hơn một pipeline webhook làm dở dang.

Lưu lượng thấp cũng nghiêng về phía đó. Poll mỗi 5 phút là 288 request một ngày, chẳng giới hạn tốc độ (rate limit) nào để ý, và bạn được sự đơn giản khi vận hành mà không mất gì. Một nhà cung cấp có webhook chập chờn cũng vậy: nếu họ gửi không ổn định và nhật ký sự kiện của họ sơ sài, poll API của họ đáng tin hơn là trông vào bên gửi của họ.

Khi nào webhook xứng với độ phức tạp của nó

Webhook đáng giá khi có người đang chờ. Xác nhận thanh toán, trạng thái deploy, một tin nhắn chat. Độ trễ dưới một giây là quan trọng, và poll đủ nhanh để giả được điều đó nghĩa là dội request liên tục vào một API.

Webhook cũng thắng khi sự kiện hiếm và khó đoán. Poll một thứ chỉ xảy ra hai lần một ngày nghĩa là khoảng 99,9% số request trả về không có gì, và đó chính là trường hợp webhook được thiết kế để giải quyết. Ở thái cực ngược lại, lưu lượng có thể khiến polling bất khả thi: mười nghìn shop, mỗi shop cần dữ liệu mới trong vòng 30 giây, là 28,8 triệu request một ngày, và bạn sẽ chạm giới hạn tốc độ từ lâu trước khi tới được con số đó.

Trường hợp cuối là các job chạy lâu. Transcode video, inference mô hình, export hàng loạt. Nhà cung cấp biết khi nào job xong còn bạn thì chỉ đoán, đó là lý do các API AI và media đẩy mạnh webhook. Xem bài Webhook cho AI agent.

Mô hình lai, thứ đằng nào bạn cũng sẽ xây

Gần như mọi tích hợp trưởng thành đều đi tới đây, và chủ động đi tới thì rẻ hơn là đi tới sau một sự cố. Webhook lo độ trễ; một vòng poll đối soát định kỳ lo tính đúng đắn.

// Fast path: webhook, near-real-time
export async function POST(req: Request) {
  const event = await verify(req);
  await upsertEvent(event);           // idempotent by provider event id
  return new Response(null, { status: 200 });
}

// Safety net: every 15 minutes, ask what we should have
async function reconcile() {
  const now = new Date();             // before listing, so nothing updated mid-sweep is skipped
  const since = await getLastReconciledAt();
  for await (const obj of provider.list({ updated_since: since })) {
    await upsertEvent(obj);           // same idempotent path, no-op if webhook won
  }
  await setLastReconciledAt(now);
}

Job đối soát chỉ tốn một function chạy theo lịch và dùng lại code bạn đã viết. Đổi lại, nó biến đặc tính tệ nhất của webhook, mất mát im lặng và vĩnh viễn, thành “cũ tối đa 15 phút”. Đó là một rủi ro hoàn toàn khác. Trường hợp xấu nhất thậm chí không phải một khoản thanh toán bị lỡ, thứ mà sớm muộn cũng có người nhận ra, mà là một access.revoked bị lỡ, thứ không ai nhận ra cả.

Điều kiện để cách này chạy được là cả hai đường đều đi qua cùng một hàm ghi idempotent. Nếu webhook đã xử lý rồi, đối soát không làm gì cả. Nếu chưa, đối soát sửa lại. Điều này chỉ đúng khi bạn chống trùng lặp dựa trên event ID của nhà cung cấp, chứ không dựa trên thời điểm nhận của bạn. Các khuôn mẫu có trong bài Bạn nhận cùng một webhook hai lần.

Sau đó đo phần chồng lấn. Đếm xem đối soát tìm thấy thứ webhook bỏ sót bao nhiêu lần. Con số đó là tỷ lệ mất mát thật của pipeline webhook, và là phép đo trung thực duy nhất bạn từng có được. Nếu nó bằng không suốt một tháng, webhook của bạn khỏe. Nếu nó tăng vọt, bạn đang có một sự cố mà nếu không đo thì sẽ chẳng bao giờ phát hiện ra.

Lựa chọn ở giữa mà người ta hay quên

Polling không nhất thiết phải ngốc nghếch.

Polling dựa trên cursor, tức là ?since=<cursor> thay vì “lấy hết rồi so khác biệt”, rẻ hơn rất nhiều và không bao giờ bỏ sót mục nào, vì cursor chỉ tiến lên khi xử lý thành công.

Request có điều kiện, If-None-Match với ETag hoặc If-Modified-Since, trả về 304 không có body. Nhiều nhà cung cấp không tính các request đó vào giới hạn tốc độ của bạn. Bạn có thể poll dày đặc mà tốn rất ít.

Long polling cho bạn độ trễ gần như webhook kèm khả năng khôi phục của mô hình kéo. Nó hiếm hơn, nhưng ở đâu có thì đó thường là lựa chọn tốt nhất của cả hai bên.

Nếu bạn từng gạt polling đi vì chi phí lấy toàn bộ dữ liệu theo chu kỳ cố định, có thể bạn đã so với phiên bản tệ nhất của nó.

AnyHook giúp được gì

Webhook hỏng trong im lặng vì không ai giữ bản ghi về những gì lẽ ra phải tới. AnyHook chính là bản ghi đó.

Nhà cung cấp POST tới in.anyhook.net/you/app. Mọi sự kiện đều được lưu bền vững trước khi thử gửi, nên:

  • Khoảng trống trở nên nhìn thấy được. Các lần gửi thất bại được đếm, hiển thị và gửi cảnh báo ở mốc 1, 5 và 20 lần thất bại liên tiếp, và chữ “im lặng” không còn đúng nữa
  • Khôi phục là một lần phát lại (replay) chứ không phải một job đối soát. Chọn khoảng thời gian server của bạn ngừng hoạt động và phát lại từ nhật ký. 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)
  • Khoản thuế hạ tầng do chúng tôi trả, một lần: thử lại với exponential backoff (khoảng chờ tăng theo cấp số nhân), một nhật ký bền vững và một header AnyHook-Signature duy nhất thay vì mỗi nhà cung cấp một cơ chế xác minh riêng

Điều này không loại bỏ lý do cần một vòng poll đối soát. Nếu nhà cung cấp chưa bao giờ gửi sự kiện, không dịch vụ trung chuyển webhook (webhook relay) nào tự tạo ra nó được. Nhưng nó thu hẹp kiểu hỏng từ “im lặng và vĩnh viễn” xuống “có ghi lại, có cảnh báo và phát lại được”, tức là phần lớn những gì mô hình lai đã mang lại cho bạn.

Để 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ớ

Chọn theo kiểu hỏng. Polling chậm nhưng tự lành; webhook nhanh nhưng làm mất dữ liệu trong im lặng. Nếu độ trễ không ảnh hưởng tới con người hay tới giới hạn tốc độ, polling không phải lựa chọn cho người mới, mà là lựa chọn có trường hợp xấu nhất tốt hơn. Và nếu bạn chạy webhook trên production, hãy thêm vòng poll đối soát trước khi cần đến nó, vì con số nó tạo ra là cách duy nhất để bạn biết mình thực sự đang mất bao nhiêu sự kiện.

Tất cả bài viết3 tháng 8, 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.