Jawaban singkat untuk pertanyaan yang paling sering diajukan pembaca tentang topik ini.
01Bagaimana cara retry request yang gagal?
Retry hanya kegagalan sementara seperti timeout, connection reset, 429 dan 503, dan hanya untuk operasi idempotent atau request yang membawa idempotency key. Tunggu dengan exponential backoff ber-cap plus jitter, patuhi header Retry-After bila ada, dan batasi total percobaan dengan budget dan deadline. Jangan retry error 4xx lainnya, karena request yang sama akan gagal dengan cara yang sama.
02Apa itu exponential backoff dengan jitter?
Exponential backoff menggandakan waktu tunggu setelah setiap percobaan gagal sampai batas cap. Jitter menambahkan keacakan pada waktu tunggu itu agar banyak client tidak retry di detik yang sama. Pada full jitter, setiap client tidur selama waktu acak antara nol dan nilai exponential yang sudah di-cap.
03Kenapa jitter diperlukan kalau sudah memakai exponential backoff?
Tanpa jitter, client yang gagal bersamaan menunggu delay penggandaan yang sama lalu retry bersamaan, sehingga lonjakan beban berulang di setiap putaran. Pada simulasi 1000 client yang gagal di saat yang sama dengan window 50 ms, tanpa jitter semua 1000 retry jatuh di satu window pada tiap putaran, sedangkan full jitter menyebar putaran pertama di 4 window dan putaran keempat di 52. AWS Architecture Blog juga menyebut backoff tanpa jitter sebagai pecundang yang jelas.
04Error HTTP mana yang aman untuk di-retry?
Timeout, connection reset, 408, 429, 502, 503, dan 504 bersifat sementara dan layak di-retry, asalkan operasinya idempotent. Untuk 429 dan 503, tunggu setidaknya selama yang dikatakan Retry-After bila ada. Error seperti 400, 401, 403, 404, dan 422 berarti request-nya salah, jadi mengulanginya hanya menambah beban.
05Bagaimana mencegah retry menyebabkan retry storm?
Retry di satu layer saja, karena tiga layer yang masing-masing mencoba 3 kali berlipat menjadi 27 panggilan di dasar. Tambahkan retry budget, misalnya batas buku Google SRE berupa tiga percobaan per request dan retry di bawah 10% dari request per client. Lakukan backoff dengan jitter dan tetapkan deadline keseluruhan agar retry berhenti ketika sudah tidak membantu.
Retry dengan Exponential Backoff dan Jitter di TypeScript
Cara melakukan retry request yang gagal dengan aman: error mana yang di-retry, hitungan retry storm, exponential backoff dengan jitter, Retry-After, retry budget, dan idempotency key, lengkap dengan helper TypeScript.
Retry hanya kegagalan sementara pada operasi idempotent: timeout, connection reset, 429 dan 503, jangan pernah error 4xx lainnya. Tunggu dengan exponential backoff ber-cap plus full jitter agar client menyebar, bukan retry serempak. Patuhi Retry-After, retry di satu layer saja, batasi percobaan dengan budget dan deadline, dan kirim idempotency key pada write.
Bayangkan sekumpulan terminal POS dan payment provider yang mengembalikan 503 selama sepuluh detik. Setiap terminal punya loop retry, jadi semuanya retry, dan outage singkat berubah menjadi gelombang traffic yang harus ditahan provider tepat saat ia baru pulih. Retry adalah taruhan bahwa percobaan berikutnya akan berhasil. Kalau dipasang sembarangan, retry juga menjadi beban tambahan ke sistem yang memang sedang gagal.
Post ini menjawab satu pertanyaan: bagaimana cara retry request yang gagal? Isinya error mana yang layak di-retry, bagaimana retry berlipat di antara layer, bagaimana exponential backoff dan jitter menyebarkannya, serta apa tambahan dari Retry-After, budget, deadline, dan idempotency key. Aturannya bersumber dari AWS Architecture Blog, buku Google SRE, dan dua RFC. Helper retry dan simulasinya adalah kode yang saya jalankan, dan outputnya ditempel apa adanya. Post circuit breaker dan idempotency key di situs ini membahas dua topik tetangganya secara mendalam.
Error mana yang sebaiknya di-retry, dan mana yang tidak boleh?
Retry kegagalan yang bersifat sementara dan operasi yang aman diulang. Dua syarat itu sama pentingnya. Timeout memang sementara, tetapi kalau request-nya pembayaran, Anda tidak tahu apakah server sudah mengerjakannya. RFC 9110 mendefinisikan sebuah method sebagai idempotent bila mengulangnya punya efek yang sama dengan mengirimnya sekali, dan menyebut PUT, DELETE, dan safe method sebagai idempotent. POST tidak ada di daftar itu.
Sinyal
Retry?
Alasan
Timeout atau connection reset
Ya, bila idempotent atau dikirim dengan idempotency key
Anda tidak bisa tahu apakah server sudah memproses request sebelum koneksi putus.
429 Too Many Requests
Ya, setelah menunggu sesuai permintaan server
RFC 6585 mendefinisikannya sebagai rate limiting dan membolehkan header Retry-After untuk lama tunggu.
503 Service Unavailable
Ya, dengan mematuhi Retry-After
RFC 9110 menyebut Retry-After pada 503 menunjukkan berapa lama layanan diperkirakan tidak tersedia.
502 atau 504 dari proxy
Ya, bila idempotent
Proxy-nya yang gagal, dan origin di belakangnya mungkin sudah atau belum menangani request.
400, 401, 403, 404, 422
Tidak
Request-nya sendiri yang salah. Mengirim byte yang sama gagal dengan cara yang sama dan hanya menambah beban.
Error 500 biasa berada di tengah. Sering itu bug di handler yang akan terkena lagi saat retry, jadi retry paling banyak sekali dan hanya bila operasinya idempotent. Kasus 401 adalah satu-satunya pengecualian aturan 4xx: refresh token, lalu kirim request baru. Itu tindakan yang berbeda dari retry, jadi jangan menghabiskan jatah retry budget untuk itu.
Apa itu retry storm, dan bagaimana retry berlipat di antara layer?
Retry storm adalah beban yang diciptakan oleh retry itu sendiri. Penyebabnya adalah layering: gateway memanggil orders, orders memanggil payments, payments memanggil bank API. Kalau tiap layer mencoba sampai 3 kali, panggilan yang terus gagal dicoba 3 kali di setiap layer, dan perkaliannya terjadi di bawah layer pertama. Buku Google SRE menyebutnya combinatorial explosion. Hitungannya ada di bawah, dan itu hasil penurunan, bukan pengukuran.
Gateway -> Orders -> Payments -> Bank API
3 tries 3 tries 3 tries (every layer retries a failed call up to 2 times)
Calls reaching the Bank API per user request when it keeps failing:
1 layer retrying: 3
2 layers retrying: 3 x 3 = 9
3 layers retrying: 3 x 3 x 3 = 27
4 layers retrying: 3 x 3 x 3 x 3 = 81
A budget of 10% retries per client instead (SRE book): about 1.1x, not 27x.
Obat dari buku SRE ada dua bagian. Pertama, retry hanya di layer tepat di atas layer yang menolak request, sehingga layer tidak saling mengalikan. Kedua, pasang budget untuk retry. Buku itu menjelaskan batas per request sampai tiga percobaan dan batas per client yang hanya mengizinkan retry selama rasio retry di bawah 10% dari request. Tanpa batas client, traffic tumbuh sampai sedikit di bawah 3x laju awal, dan dengan batas itu sekitar 1.1x. Bila sebagian besar task overloaded, buku itu menyarankan berhenti retry dan membiarkan error naik sampai ke pemanggil.
Layered retry bersembunyi di library. HTTP client, consumer message queue, dan API gateway masing-masing bisa retry secara default, dan tak ada yang melihat hasil kalinya sampai service di bawah tumbang. Daftar semua layer antara user dan dependency yang gagal, lalu putuskan satu layer yang melakukan retry.
Bagaimana exponential backoff bekerja, dan kenapa itu belum cukup?
Exponential backoff menggandakan waktu tunggu setelah setiap percobaan gagal, sampai batas cap. Dengan base 100 ms, delay pada empat retry pertama di helper di bawah adalah 200, 400, 800, dan 1600 ms, dan cap mencegah outage panjang menghasilkan sleep berjam-jam. Idenya sudah lama, dan artikel Wikipedia tentang exponential backoff menelusurinya ke penanganan collision di jaringan bersama. Intinya, client yang terus gagal sebaiknya bertanya makin jarang.
Kelemahannya, semua client menggandakan waktu secara serempak. Kalau seribu client gagal di saat yang sama, semuanya menunggu tepat 200 ms, retry bersama, gagal bersama, lalu semuanya menunggu tepat 400 ms. Post AWS Architecture Blog oleh Marc Brooker menyebut exponential backoff polos tanpa jitter sebagai pecundang yang jelas, karena butuh lebih banyak kerja dan waktu dibanding varian ber-jitter. Delay-nya tumbuh, tetapi lonjakannya tidak menyebar, jadi server melihat satu gelombang request di setiap putaran.
Jitter mana yang sebaiknya dipakai: full, equal, atau decorrelated?
Jitter menambahkan keacakan pada waktu tunggu agar client berhenti retry serempak. Post AWS membandingkan tiga versi. Full jitter memilih delay acak antara nol dan nilai exponential yang sudah di-cap. Equal jitter mempertahankan separuh backoff dan mengacak separuh sisanya. Decorrelated jitter menurunkan maksimum berikutnya dari sleep sebelumnya, bukan dari jumlah percobaan.
Strategi
Sleep sebelum retry ke-n
Yang menonjol
Tanpa jitter
min(cap, base x 2 pangkat n)
Semua client bangun di detik yang sama. AWS menyebutnya pecundang yang jelas.
Full jitter
acak dari 0 sampai min(cap, base x 2 pangkat n)
Sebaran terlebar di antara varian ber-cap, tetapi client bisa mendapat sleep mendekati nol.
Equal jitter
separuh backoff ditambah acak sampai separuh sisanya
Menjamin waktu tunggu minimum. AWS melaporkan kerja sedikit lebih banyak dari full jitter dan waktu selesai jauh lebih lama.
Decorrelated jitter
min(cap, acak dari base sampai sleep sebelumnya x 3)
Tumbuh dari sleep terakhir. AWS melaporkan jumlah panggilannya lebih banyak dari full jitter.
Supaya clustering terlihat dan bukan sekadar klaim, saya mensimulasikan 1000 client yang semuanya gagal pada t=0 lalu menghitung berapa retry yang jatuh di tiap window 50 ms. Simulasi memakai fungsi backoffDelay yang sama dengan helper di bagian berikutnya, base 100 ms, cap 10 detik, dan random generator ber-seed agar bisa diulang. Yang diukur hanya clustering. Ini tidak mengatakan apa pun tentang performa server sungguhan.
// simulate.ts: 1000 clients all fail at t=0. For retries 1 to 4, each client sleeps
// backoffDelay(...) after its own previous retry; count how many land in each 50 ms window.
// base = 100 ms, cap = 10000 ms, seeded PRNG (mulberry32, seed 42) so the run repeats.
for (const strategy of ["none", "full", "equal", "decorrelated"]) { /* ... */ }
$ node simulate.ts
clients=1000 all fail at t=0, base=100ms cap=10000ms, window=50ms, seed=42
none
retry 1: occupied windows=1, peak retries in one window=1000
retry 2: occupied windows=1, peak retries in one window=1000
retry 3: occupied windows=1, peak retries in one window=1000
retry 4: occupied windows=1, peak retries in one window=1000
full
retry 1: occupied windows=4, peak retries in one window=277
retry 2: occupied windows=12, peak retries in one window=147
retry 3: occupied windows=26, peak retries in one window=72
retry 4: occupied windows=52, peak retries in one window=48
equal
retry 1: occupied windows=2, peak retries in one window=520
retry 2: occupied windows=6, peak retries in one window=265
retry 3: occupied windows=14, peak retries in one window=126
retry 4: occupied windows=26, peak retries in one window=79
decorrelated
retry 1: occupied windows=4, peak retries in one window=277
retry 2: occupied windows=20, peak retries in one window=124
retry 3: occupied windows=57, peak retries in one window=52
retry 4: occupied windows=116, peak retries in one window=33
Tanpa jitter, ke-1000 retry jatuh di satu window pada setiap putaran. Dengan full jitter, putaran retry pertama menyebar di 4 window dengan puncak 277, dan pada putaran keempat di 52 window dengan puncak 48. Equal jitter lebih rapat, yaitu 2 window dan puncak 520 di putaran pertama. Decorrelated jitter paling lebar pada putaran keempat, tersebar di 116 window. Post AWS menambahkan catatan yang layak diingat: jitter mengurangi kerja, tetapi tidak ada varian yang mengubah sifat kuadratik dari banyak client yang berebut satu server.
Mulai dengan full jitter. Hanya satu baris, tidak butuh state per client, dan AWS melaporkan jumlah panggilannya lebih sedikit dari decorrelated jitter. Pindah hanya bila Anda sudah mengukur alasannya, dan pertahankan cap, karena jitter tidak membatasi lama tunggu.
Bagaimana Retry-After, deadline, dan retry budget digabung dalam satu helper?
Helper di bawah menggabungkan semua bagiannya. Ia hanya me-retry error yang ada di tabel, memilih strategi, memperlakukan Retry-After sebagai batas bawah, memberi tiap percobaan hanya sisa dari deadline keseluruhan, dan menyerah daripada tidur melewati deadline itu. RFC 9110 menyebut Retry-After berupa jumlah detik atau HTTP-date, jadi parseRetryAfter menangani keduanya. Kodenya berjalan sebagai TypeScript biasa di Node 22.18 atau lebih baru, yang membuang tipe tanpa langkah build.
export type Strategy = "none" | "full" | "equal" | "decorrelated";
export interface RetryOptions {
maxAttempts: number; // total tries, including the first
baseMs: number;
capMs: number;
deadlineMs: number; // wall-clock budget for the whole call, retries included
strategy: Strategy;
rng?: () => number;
sleep?: (ms: number) => Promise<void>;
}
export class HttpError extends Error {
status: number;
retryAfterMs?: number;
constructor(status: number, retryAfterMs?: number) {
super("HTTP " + status);
this.status = status;
this.retryAfterMs = retryAfterMs;
}
}
// Transient by nature. 400, 401, 403, 404, 422 are absent on purpose: repeating them fails again.
const RETRYABLE_STATUS = new Set([408, 429, 502, 503, 504]);
export function isRetryable(err: unknown): boolean {
if (err instanceof HttpError) return RETRYABLE_STATUS.has(err.status);
const code = (err as { code?: string }).code;
return code === "ECONNRESET" || code === "ETIMEDOUT" || code === "ECONNREFUSED";
}
// Retry-After is either delay-seconds or an HTTP-date (RFC 9110 section 10.2.3).
export function parseRetryAfter(value: string | null, now = Date.now()): number | undefined {
if (!value) return undefined;
if (/^\d+$/.test(value)) return Number(value) * 1000;
const at = Date.parse(value);
return Number.isNaN(at) ? undefined : Math.max(0, at - now);
}
// attempt is 1 for the first retry. prev is the previous sleep (decorrelated only).
export function backoffDelay(
o: Pick<RetryOptions, "baseMs" | "capMs" | "strategy">,
attempt: number,
prev: number,
rng: () => number,
): number {
const exp = Math.min(o.capMs, o.baseMs * 2 ** attempt);
switch (o.strategy) {
case "none":
return exp;
case "full":
return rng() * exp;
case "equal":
return exp / 2 + (rng() * exp) / 2;
case "decorrelated":
return Math.min(o.capMs, o.baseMs + rng() * (prev * 3 - o.baseMs));
}
}
export async function retry<T>(
op: (attempt: number, signal: AbortSignal) => Promise<T>,
o: RetryOptions,
): Promise<T> {
const rng = o.rng ?? Math.random;
const sleep = o.sleep ?? ((ms: number) => new Promise<void>((r) => setTimeout(r, ms)));
const started = Date.now();
let prev = o.baseMs;
for (let attempt = 1; ; attempt++) {
const left = o.deadlineMs - (Date.now() - started);
if (left <= 0) throw new Error("deadline exceeded before attempt " + attempt);
try {
// Each attempt gets only what is left of the overall deadline.
return await op(attempt, AbortSignal.timeout(left));
} catch (err) {
if (!isRetryable(err) || attempt >= o.maxAttempts) throw err;
let wait = backoffDelay(o, attempt, prev, rng);
prev = wait;
// The server's hint is a floor: never retry sooner than it asked.
const hint = err instanceof HttpError ? err.retryAfterMs : undefined;
if (hint !== undefined) wait = Math.max(wait, hint);
if (wait >= o.deadlineMs - (Date.now() - started)) throw err; // would wake after the deadline
await sleep(wait);
}
}
}
// Run with: node smoke.ts (Node 22.18+ strips the types; no build step)
const slept: number[] = [];
let calls = 0;
const result = await retry(
async () => {
calls++;
if (calls < 3) throw new HttpError(503, calls === 1 ? 1500 : undefined);
return "ok";
},
{ maxAttempts: 5, baseMs: 100, capMs: 10_000, deadlineMs: 30_000,
strategy: "full", rng: () => 0.5, sleep: async (ms) => { slept.push(ms); } },
);
console.log(result, "calls=" + calls, "sleeps=" + JSON.stringify(slept));
// Real output:
// ok calls=3 sleeps=[1500,200]
// retry 1: rng 0.5 x 200 ms = 100 ms, but Retry-After said 1500 ms, so it waited 1500
// retry 2: rng 0.5 x 400 ms = 200 ms, no hint
// A 404 is not retried at all:
// 404 calls=1 Error: HTTP 404
Saya menjalankan smoke test dengan nilai random tetap 0.5 dan sleep palsu. Sebuah 503 dengan hint 1500 ms, disusul 503 biasa lalu sukses, memakai tiga panggilan dan tidur 1500 lalu 200 ms: delay pertama yang dihitung adalah 100 ms, tetapi hint server menang. Sebuah 404 hanya membuat satu panggilan dan langsung di-throw. Helper ini tidak punya retry budget lintas panggilan. Penghitung bersama untuk retry terhadap request, seperti di buku SRE, sebaiknya berada di client yang memegang connection pool, bukan di setiap titik pemanggilan.
Bagaimana idempotency key membuat retry aman?
Idempotency key mengubah write yang tidak idempotent menjadi aman diulang. Client membuat satu key per operasi logis, mengirimnya di setiap percobaan, dan server mengembalikan hasil tersimpan untuk key yang sudah pernah dilihat alih-alih mengerjakannya dua kali. Detail yang penting adalah di mana key dibuat. Key harus ada sebelum percobaan pertama dan dipakai ulang oleh setiap retry.
// One key per LOGICAL operation, created before the first attempt and reused on every retry.
const key = crypto.randomUUID();
await retry(
(attempt, signal) =>
fetchOrThrow("https://api.example.com/v1/payments", {
method: "POST", // not idempotent by itself; the key makes the retry safe
headers: { "Idempotency-Key": key, "Content-Type": "application/json" },
body: JSON.stringify({ orderId: "ord_1042", amountIdr: 85000 }),
signal,
}),
{ maxAttempts: 3, baseMs: 200, capMs: 5_000, deadlineMs: 10_000, strategy: "full" },
);
// Wrong: a new key inside the callback. Every retry is then a brand-new payment.
Kalau key dibuat di dalam callback yang di-retry, setiap percobaan menjadi operasi baru dan timeout yang disusul retry bisa menagih pelanggan dua kali. Key juga menghilangkan ambiguitas berbahaya dari tabel pertama: setelah timeout Anda bisa me-retry pembayaran tanpa tahu apakah sebelumnya sudah berhasil. Post idempotency keys di situs ini membahas sisi server, yaitu menyimpan key dan responsnya, dan post circuit breaker membahas apa yang dilakukan ketika retry terus gagal dan Anda sebaiknya berhenti memanggil sama sekali.
Apa checklist sebelum Anda merilis retry?
Jalani urutan ini. Masing-masing menghapus satu failure mode dari bagian di atas, dan melewatkan salah satunya adalah cara retry yang membantu berubah menjadi penguat outage.
Retry hanya timeout, connection reset, 408, 429, 502, 503, dan 504, dan jangan pernah error 4xx lainnya.
Retry hanya operasi idempotent, atau lampirkan idempotency key yang dibuat sebelum percobaan pertama.
Pakai exponential backoff ber-cap dengan full jitter, dan perlakukan Retry-After sebagai waktu tunggu minimum.
Pilih satu layer untuk retry dan matikan retry di semua layer lain antara user dan dependency.
Batasi percobaan, dan kekang retry dengan budget seperti 10% dari request, agar outage panjang tidak melipatgandakan traffic.
Tetapkan deadline keseluruhan, teruskan sisa waktunya ke tiap percobaan, dan berhenti saat sleep berikutnya melewatinya.
Aturan yang saya bawa pulang: retry adalah permintaan kapasitas tambahan ke service yang sedang kesulitan, jadi ia harus jarang, tersebar, dan aman diulang. Retry error yang tepat, tambahkan jitter, patuhi Retry-After, retry di satu tempat, pasang budget dan deadline, dan buat write idempotent. Dengan begitu, outage singkat tetap singkat dan tidak berubah menjadi badai.