TL;DR
- Backoff eksponensial yang naif (
delay = base * 2^attempt) menciptakan thundering herd, karena setiap klien mencoba ulang secara bersamaan. - Tambahkan jitter (keacakan) untuk menyebar percobaan ulang (retry). Equal jitter (
base/2 + random(base/2)) adalah titik idealnya. - Batasi jeda maksimum, batasi total durasi percobaan ulang, dan pisahkan 4xx (jangan dicoba ulang) dari 5xx (coba ulang).
- Aktifkan circuit breaker di level tujuan setelah sekitar 20 kegagalan berturut-turut. Percobaan ulang tidak akan memperbaiki endpoint yang rusak.
- Putar ulang (replay) yang murah mengalahkan percobaan ulang yang agresif: jika memutar ulang cukup satu klik, Anda hanya butuh percobaan ulang untuk menangani kegagalan sementara.
Sebagian besar kode percobaan ulang yang ditemui di lapangan terlihat seperti ini:
for (let i = 0; i < maxRetries; i++) {
try {
return await deliver(event);
} catch {
await sleep(1000 * Math.pow(2, i)); // 1s, 2s, 4s, 8s...
}
}
Ini backoff eksponensial, dan lebih baik daripada percobaan ulang dengan interval tetap. Tetapi ini juga salah dengan cara yang hanya muncul di production, saat ratusan webhook gagal bersamaan dan semuanya mencoba ulang pada saat yang sama, membanting server Anda yang sedang pulih kembali ke tanah.
Perbaikannya adalah jitter. Berikut alasannya, dan cara melakukannya dengan benar.
Thundering herd
Misalkan endpoint Anda mati pada T+0. Dalam 60 detik berikutnya, 500 webhook Stripe tiba dan semuanya gagal. Setiap antrean percobaan ulang kini berisi 500 percobaan ulang pertama yang dijadwalkan pada T+5min (atau berapa pun jeda dasar Anda), 500 percobaan ulang kedua pada T+10min, dan seterusnya.
Endpoint Anda kembali online pada T+4min. Selamat: kini Anda kedatangan 500 percobaan ulang serentak pada T+5min, yang mendorong server Anda yang masih memulihkan diri kembali melewati titik jenuhnya. Itulah thundering herd.
Backoff deterministik punya masalah ini sejak dari desainnya. Setiap klien atau event yang gagal pada waktu yang sama akan mencoba ulang pada waktu yang sama.
Jitter
Jitter menambahkan keacakan pada waktu percobaan ulang sehingga event dicoba ulang secara tersebar, bukan menumpuk dalam satu lonjakan. Tiga strategi yang umum:
Full jitter
const base = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s, 8s
const sleep = Math.random() * base; // 0 to base
Setiap percobaan ulang menunggu antara 0 hingga interval backoff penuh. Sebarannya maksimal, tetapi sebagian percobaan ulang terjadi hampir seketika, yang bisa terasa tidak efisien.
Equal jitter
const base = 1000 * Math.pow(2, attempt);
const sleep = base / 2 + Math.random() * (base / 2);
Setiap percobaan ulang menunggu antara setengah hingga penuh interval backoff. Batas bawahnya terjaga, sebarannya tetap baik.
Decorrelated jitter
let sleep = baseDelay;
// on each retry:
sleep = Math.min(maxDelay, baseDelay + Math.random() * (sleep * 3 - baseDelay));
Jeda setiap percobaan ulang adalah kelipatan acak dari jeda sebelumnya. Cocok jika Anda ingin backoff bertambah tanpa rumus tetap.
Untuk webhook, equal jitter adalah titik idealnya. Percobaan ulang tetap kurang lebih eksponensial, sementara thundering herd teredam.
Seperti apa jadwal percobaan ulang yang layak untuk production
Di luar jitter, inilah yang dilakukan tim berpengalaman.
Batasi jeda maksimum. Jangan biarkan upaya ke-10 menunggu 17 menit. Tetapkan batas atas, misalnya 60 detik, dan setelah mencapai batas itu, tetap di sana sampai Anda menyerah.
Batasi total durasi percobaan ulang, bukan hanya jumlah upaya. “Coba ulang selama 24 jam” adalah SLA yang lebih jelas daripada “coba ulang 8 kali”, karena yang kedua bergantung pada hitungan jeda yang tidak diingat siapa pun.
Pisahkan error sementara dari error permanen. Respons 4xx dari tujuan, terutama 401, 403, atau 404, tidak akan berhasil jika dicoba ulang. Server sedang memberi tahu Anda bahwa permintaannya salah. Coba ulang hanya untuk 5xx, 429, timeout, dan error koneksi.
Tampilkan status percobaan ulang. Pengguna Anda perlu tahu bahwa sebuah event sedang berada di upaya ke-3 dari 5, dengan percobaan ulang berikutnya pada waktu tertentu. Jika tidak terlihat, debugging jadi menyiksa.
Pasang circuit breaker di level tujuan. Jika sebuah tujuan sudah gagal 20 kali berturut-turut, berhenti mencobanya ulang untuk sementara. Ada yang salah dan percobaan ulang tidak akan memperbaikinya, dan Anda hanya membakar komputasi.
Cara AnyHook menanganinya
Kebijakan percobaan ulang AnyHook disesuaikan per paket dan memakai infrastruktur percobaan ulang terkelola dari QStash di baliknya. Upaya pengiriman berjumlah 3 di Free, 5 di Pro, dan 10 di Scale, dengan tangga eksponensial tetap: 5 menit, 30 menit, 3 jam, 18 jam, lalu 24 jam untuk setiap upaya sesudahnya. Tidak ada jitter yang ditambahkan pada jeda tersebut. Batas waktu pengiriman masing-masing 60 detik, 120 detik, dan 5 menit. Setelah 20 kegagalan berturut-turut, circuit breaker terpicu dan menjeda pengiriman ke tujuan itu (10 menit, lalu 30 menit, 2 jam, dan 24 jam jika terpicu berulang) serta mengirimi Anda email, jadi tidak ada banjir percobaan ulang. Respons 4xx selain 408 dan 429 sama sekali tidak dicoba ulang, dan peringatan kegagalannya menjelaskan alasannya, karena percobaan ulang tidak akan membantu di sana dan Anda perlu memperbaiki bentuk permintaannya.
Percobaan ulang yang habis tidak menghilangkan event. Event tetap tersimpan di log Anda dengan status=failed, dan Anda bisa memutarnya ulang secara manual dari dashboard atau API kapan pun dalam masa putar ulang paket Anda (3 hari di Free, 30 di Pro, 90 di Scale).
Jalan pintasnya
Wawasan yang mengubah cara Anda memandang percobaan ulang: Anda tidak butuh percobaan ulang yang agresif jika punya putar ulang yang murah.
Jika memutar ulang cukup satu klik, mencoba ulang 3 kali dalam satu jam sudah cukup. Sebagian besar kegagalan sementara tertangani secara otomatis, dan untuk gangguan panjang yang jarang terjadi, seseorang memutar ulang batch yang gagal setelah server sehat kembali.
Bandingkan dengan sistem yang hanya mengandalkan percobaan ulang, ketika Anda memperpanjang rentang percobaan ulang sampai 72 jam untuk berjaga-jaga, yang berarti 72 jam pengiriman sia-sia ke endpoint yang sudah Anda tahu sedang mati.
Putar ulang yang murah jelas lebih baik daripada percobaan ulang yang agresif, dan prinsip itulah yang menjadi dasar AnyHook.
Kesimpulan
Perlakukan percobaan ulang sebagai jalur cepat untuk kegagalan sementara, bukan pengganti persistensi dan putar ulang. Sistem yang tetap bertahan di minggu yang buruk adalah sistem yang pengiriman gagalnya selalu bisa dipulihkan, karena event-nya sudah dicatat sebelum pernah dikirim.
Jadwal sebenarnya dari setiap pengirim ada di referensi provider webhook kami. Percobaan ulang juga punya batas keras yang tidak dikendalikan siapa pun: jika terus gagal, Shopify bisa menghapus langganannya, sementara Stripe menonaktifkan endpoint-nya. Dan percobaan ulang itulah alasan duplikat dan pengiriman yang tidak berurutan pasti terjadi, bukan hal langka: Anda Menerima Webhook yang Sama Dua Kali.