Retry Strategy #
Di sistem terdistribusi, tidak semua kegagalan bermakna ada yang salah secara fundamental. Sebagian besar kegagalan bersifat transient — koneksi database yang sesaat penuh, API eksternal yang sedang melayani traffic spike, DNS lookup yang gagal satu kali tapi berhasil di percobaan berikutnya. Jika sistem langsung menyerah pada kegagalan pertama, banyak operasi yang seharusnya bisa berhasil akan menjadi error yang tidak perlu. Tapi jika sistem mencoba lagi tanpa strategi yang tepat — tanpa batasan, tanpa delay, tanpa mempertimbangkan jenis error — retry itu sendiri bisa menjadi sumber masalah: membombardir downstream yang sedang berjuang untuk pulih, atau mengeksekusi operasi non-idempotent dua kali dengan konsekuensi yang tidak diinginkan. Artikel ini membahas retry strategy secara menyeluruh: enam jenis retry dengan karakteristik dan trade-off masing-masing, implementasi konkret di Go dan Dart, klasifikasi error yang boleh dan tidak boleh di-retry, hingga integrasi dengan circuit breaker dan DLQ.
Apa Itu Retry Strategy? #
Retry Strategy adalah pendekatan sistematis untuk mencoba kembali operasi yang gagal, dengan aturan dan batasan yang jelas. Bukan sekadar loop yang mengulangi operasi sampai berhasil, melainkan mekanisme yang sadar tentang kapan retry masuk akal, berapa lama jeda yang diperlukan, dan kapan harus menyerah.
// ANTI-PATTERN: retry tanpa strategi — berbahaya
func callServiceDangerous() error {
for { // ← tidak ada batas
err := callService()
if err == nil { return nil }
// tidak ada delay — DDoS ke diri sendiri
// tidak ada klasifikasi error — retry validation error yang tidak akan berubah
}
}
// BENAR: retry dengan konfigurasi eksplisit
func callServiceSafe(ctx context.Context) error {
return RetryWithBackoff(ctx, RetryConfig{
MaxAttempts: 5,
BaseDelay: 100 * time.Millisecond,
MaxDelay: 30 * time.Second,
Jitter: true,
IsRetryable: isTransientError,
}, func() error {
return callService()
})
}
// Error 4xx → fail fast tanpa retry
// Error 5xx/timeout → retry dengan increasing delay
// Setelah max attempts → return final error
flowchart TD
A[Operasi Gagal] --> B{IsRetryable?}
B -- Tidak\n4xx, validation --> C["Fail Fast\nReturn Error"]
B -- Ya\n5xx, timeout, 429 --> D{"Attempt <=\nMaxAttempts?"}
D -- Tidak --> E["Exhausted\nKirim ke DLQ\natau Return Error"]
D -- Ya --> F["Hitung Delay\nexponential + jitter"]
F --> G[Tunggu Delay]
G --> H[Retry Operasi]
H --> I{Berhasil?}
I -- Ya --> J[Sukses ✓]
I -- Tidak --> BMengapa Retry Strategy Penting #
Ada tiga alasan fundamental yang membuat retry bukan opsional di sistem modern.
Kegagalan transient adalah norma, bukan pengecualian. Network timeout, DNS hiccup, service overload sementara, cold start di serverless, lock contention yang segera terlepas — semua ini adalah kegagalan yang sembuh dengan sendirinya dalam hitungan milidetik hingga detik. Tanpa retry, setiap kegagalan transient menjadi visible error yang harus ditangani caller atau, lebih buruk, menjadi noise yang membanjiri alert.
Retry adalah fondasi resilience pattern. Circuit breaker, bulkhead, dan timeout semuanya berasumsi bahwa ada mekanisme retry di baliknya. Setelah circuit tertutup kembali usai periode cooldown, sistem harus bisa retry operasi yang sebelumnya gagal. Tanpa retry yang benar, pattern-pattern ini kurang efektif.
Reliability tanpa intervensi manual. Sistem yang tidak bisa self-heal dari kegagalan transient membutuhkan on-call engineer untuk merespons setiap incident kecil. Retry yang dirancang baik mengurangi incident rate dan membuat sistem lebih mandiri.
Enam Jenis Retry Strategy #
1. Immediate Retry #
Retry langsung tanpa jeda. Hanya cocok untuk operasi in-memory atau kasus di mana kegagalan benar-benar bersifat satu-kali dan sembuh dalam mikrodetik.
// ANTI-PATTERN: immediate retry untuk network call
for attempt := 0; attempt < maxAttempts; attempt++ {
err := callExternalService()
if err == nil { return nil }
// ← tidak ada delay
// → saat downstream down 100ms, semua retry langsung flood bersamaan
}
// BENAR: immediate retry hanya untuk operasi in-memory yang benar-benar microsecond
for attempt := 0; attempt < 3; attempt++ {
if acquired := mutex.TryLock(); acquired {
return nil
}
// mutex contention sembuh dalam nanosecond, immediate retry aman di sini
runtime.Gosched() // yield CPU, coba lagi segera
}
Jangan gunakan immediate retry untuk network call atau operasi I/O. Jika downstream down selama 100ms, semua instance yang mengalami error bersamaan akan menghantam downstream secara serentak saat mereka retry — memperparah kondisi yang sudah overload.
2. Fixed Delay Retry #
Retry dengan jeda waktu yang sama setiap kali. Lebih aman dari immediate retry tapi masih berpotensi thundering herd.
// Fixed delay — sederhana, cocok untuk sistem sederhana dengan satu instance
func retryFixed(ctx context.Context, maxAttempts int, delay time.Duration,
fn func() error) error {
var lastErr error
for attempt := 1; attempt <= maxAttempts; attempt++ {
lastErr = fn()
if lastErr == nil {
return nil
}
if attempt < maxAttempts {
select {
case <-time.After(delay):
case <-ctx.Done():
return ctx.Err()
}
}
}
return fmt.Errorf("all %d attempts failed: %w", maxAttempts, lastErr)
}
// ANTI-PATTERN: ratusan instance retry dengan delay persis sama
// → thundering herd: semua 200 instance retry setelah tepat 1 detik
// → downstream menerima spike 200 request sekaligus, bukan spread merata
3. Exponential Backoff #
Delay berlipat ganda setiap kali retry gagal, memberi downstream waktu yang semakin panjang untuk pulih.
// Delay: 100ms → 200ms → 400ms → 800ms → 1600ms (capped)
func calculateBackoff(attempt int, base, max time.Duration) time.Duration {
delay := base * time.Duration(1<<uint(attempt-1)) // base × 2^(attempt-1)
if delay > max {
delay = max
}
return delay
}
flowchart LR
A["Attempt 1\nFail"] -->|"100ms"| B["Attempt 2\nFail"]
B -->|"200ms"| C["Attempt 3\nFail"]
C -->|"400ms"| D["Attempt 4\nFail"]
D -->|"800ms"| E["Attempt 5\nFail/Success"]
style A fill:#ffcccc
style B fill:#ffcccc
style C fill:#ffcccc
style D fill:#ffccccJauh lebih baik dari fixed delay, tapi masih thundering herd jika banyak instance mulai retry secara bersamaan karena semua menggunakan delay yang identik.
4. Exponential Backoff + Jitter — Best Practice #
Menambahkan randomisasi pada delay untuk menyebar timing retry secara alami. Ini adalah strategi yang paling direkomendasikan untuk hampir semua production use case.
// Implementasi lengkap: exponential backoff + full jitter
func RetryWithBackoff(ctx context.Context, cfg RetryConfig, fn func() error) error {
var lastErr error
for attempt := 1; attempt <= cfg.MaxAttempts; attempt++ {
lastErr = fn()
if lastErr == nil {
if attempt > 1 {
log.Infof("retry succeeded on attempt %d", attempt)
}
return nil
}
// Fail fast untuk error yang tidak layak di-retry
if !cfg.IsRetryable(lastErr) {
return fmt.Errorf("non-retryable error: %w", lastErr)
}
if attempt == cfg.MaxAttempts {
break // jangan tunggu setelah attempt terakhir
}
// Hitung base delay: min(base × 2^(attempt-1), maxDelay)
baseDelay := cfg.BaseDelay * time.Duration(1<<uint(attempt-1))
if baseDelay > cfg.MaxDelay {
baseDelay = cfg.MaxDelay
}
// Full jitter: random antara 0 dan baseDelay
// Lebih efektif daripada ±25% dalam menghindari thundering herd
jitter := time.Duration(rand.Int63n(int64(baseDelay)))
log.Warnf("attempt %d/%d failed: %v — retrying in %v",
attempt, cfg.MaxAttempts, lastErr, jitter)
select {
case <-time.After(jitter):
case <-ctx.Done():
return ctx.Err()
}
}
return fmt.Errorf("exhausted %d attempts: %w", cfg.MaxAttempts, lastErr)
}
type RetryConfig struct {
MaxAttempts int
BaseDelay time.Duration
MaxDelay time.Duration
IsRetryable func(error) bool
}
Perbandingan delay dengan dan tanpa jitter:
| Attempt | Tanpa Jitter | Dengan Full Jitter |
|---|---|---|
| 1 → 2 | 100ms (semua instance) | 0–100ms (random per instance) |
| 2 → 3 | 200ms (semua instance) | 0–200ms (random per instance) |
| 3 → 4 | 400ms (semua instance) | 0–400ms (random per instance) |
| 4 → 5 | 800ms (semua instance) | 0–800ms (random per instance) |
Dengan jitter, 200 instance yang gagal bersamaan tidak akan semua retry di waktu yang sama — mereka tersebar secara alami sepanjang window delay.
5. Retry dengan Deadline / Timeout Budget #
Alih-alih membatasi jumlah attempt, batasi total waktu yang boleh dipakai. Sangat cocok untuk sistem latency-sensitive di mana user experience bergantung pada respons cepat.
// Retry sampai deadline — bukan sampai max attempt
func RetryUntilDeadline(ctx context.Context, timeout time.Duration,
fn func() error) error {
deadline := time.Now().Add(timeout)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
attempt := 0
baseDelay := 50 * time.Millisecond
for {
attempt++
err := fn()
if err == nil {
return nil
}
remaining := time.Until(deadline)
if remaining <= 0 {
return fmt.Errorf("deadline exceeded after %d attempts: %w", attempt, err)
}
nextDelay := baseDelay * time.Duration(1<<uint(attempt-1))
// Jangan retry jika delay berikutnya lebih dari setengah remaining time
if nextDelay > remaining/2 {
return fmt.Errorf("insufficient time budget for retry: %w", err)
}
jitter := time.Duration(rand.Int63n(int64(nextDelay)))
select {
case <-time.After(jitter):
case <-ctx.Done():
return ctx.Err()
}
}
}
6. Retry dengan Circuit Breaker #
Circuit breaker dan retry bekerja bersama: retry menangani kegagalan transient, circuit breaker mencegah retry yang sia-sia saat downstream sudah jelas tidak bisa diakses.
sequenceDiagram
participant C as Caller
participant CB as Circuit Breaker
participant D as Downstream
C->>CB: Attempt 1
CB->>D: Forward (CLOSED)
D-->>CB: Error
CB-->>C: Error → retry
C->>CB: Attempt 2
CB->>D: Forward (CLOSED)
D-->>CB: Error
CB-->>C: Error → retry
C->>CB: Attempt 3
CB->>D: Forward (CLOSED)
D-->>CB: Error
Note over CB: failure rate > threshold\nCircuit OPEN
C->>CB: Attempt 4
CB-->>C: Fail fast — circuit OPEN\ntanpa panggil downstream
Note over CB: Setelah cooldown: HALF-OPEN
C->>CB: Attempt 5
CB->>D: Satu probe request
D-->>CB: Success
Note over CB: Circuit CLOSED kembali
CB-->>C: Success ✓Klasifikasi Error: Boleh dan Tidak Boleh di-Retry #
Ini adalah keputusan paling kritis dalam merancang retry strategy. Retry error yang salah bisa menyembunyikan bug atau memperparah situasi.
// Fungsi klasifikasi error yang eksplisit
func isRetryableError(err error) bool {
if err == nil {
return false
}
// Context cancelled atau deadline exceeded — jangan retry
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
return false
}
var httpErr *HTTPError
if errors.As(err, &httpErr) {
switch httpErr.StatusCode {
// ✓ BOLEH retry — error transient
case 429: return true // Too Many Requests — tunggu dan coba lagi
case 500: return true // Internal Server Error — mungkin transient
case 502: return true // Bad Gateway — upstream sedang restart
case 503: return true // Service Unavailable — downstream overload
case 504: return true // Gateway Timeout
// ✗ JANGAN retry — error deterministic
case 400: return false // Bad Request — request salah, tidak akan berubah
case 401: return false // Unauthorized — perlu refresh token dulu
case 403: return false // Forbidden — tidak punya akses
case 404: return false // Not Found — resource tidak ada
case 409: return false // Conflict — butuh logic khusus, bukan sekedar retry
case 422: return false // Unprocessable Entity — data tidak valid
}
}
// Network errors transient — biasanya layak di-retry
var netErr *net.OpError
if errors.As(err, &netErr) {
return true
}
return false // default: jangan retry jika tidak yakin
}
Ringkasan klasifikasi:
| HTTP Status | Retryable? | Alasan |
|---|---|---|
| 400 Bad Request | ❌ Tidak | Request salah — tidak akan berubah kalau diulang |
| 401 Unauthorized | ❌ Tidak | Perlu refresh token terlebih dahulu |
| 403 Forbidden | ❌ Tidak | Tidak punya akses, retry tidak akan membantu |
| 404 Not Found | ❌ Tidak | Resource tidak ada |
| 409 Conflict | ❌ Tidak | Butuh logic resolusi, bukan sekedar retry |
| 422 Unprocessable | ❌ Tidak | Data tidak valid |
| 429 Too Many Requests | ✅ Ya | Rate limited — tunggu dan coba lagi |
| 500 Server Error | ✅ Ya | Mungkin transient |
| 502 Bad Gateway | ✅ Ya | Upstream sedang restart |
| 503 Unavailable | ✅ Ya | Downstream overload sementara |
| 504 Gateway Timeout | ✅ Ya | Timeout — mungkin berhasil jika retry |
| Network timeout | ✅ Ya | Transient connectivity issue |
| context.Canceled | ❌ Tidak | User/caller sudah cancel — buang |
Rule of thumb: jika menjalankan operasi yang sama dengan input yang sama tidak mungkin menghasilkan hasil berbeda, jangan retry. Validation error dan authorization error hampir tidak pernah layak di-retry tanpa mengubah sesuatu terlebih dahulu.
Retry dan Idempotency — Hubungan yang Tidak Bisa Dipisahkan #
Retry tanpa mempertimbangkan idempotency adalah bug laten. Setiap operasi yang di-retry harus aman untuk dieksekusi lebih dari sekali.
// ANTI-PATTERN: retry operasi yang tidak idempotent
func processPayment(userID string, amount int64) error {
return RetryWithBackoff(ctx, defaultConfig, func() error {
// Jika request berhasil tapi response timeout sebelum sampai,
// retry akan memproses PAYMENT KEDUA — double charge!
return paymentGateway.Charge(userID, amount)
})
}
// BENAR: idempotency key memastikan retry selalu aman
func processPayment(idempotencyKey, userID string, amount int64) error {
return RetryWithBackoff(ctx, defaultConfig, func() error {
return paymentGateway.Charge(ChargeRequest{
IdempotencyKey: idempotencyKey, // ← gateway dedup berdasarkan key ini
UserID: userID,
Amount: amount,
})
// Jika request sebelumnya sudah berhasil, gateway return hasil yang sama
// tanpa memproses ulang — retry 100% aman
})
}
Aturan wajib: sebelum menambahkan retry ke sebuah operasi, pastikan dulu operasi tersebut idempotent — atau buat ia idempotent dengan idempotency key. Retry tanpa idempotency pada operasi finansial atau state-mutation adalah resep double processing yang bisa sangat mahal biayanya.
Retry di Berbagai Layer Sistem #
Salah satu kesalahan yang sering terjadi adalah menambahkan retry di setiap layer tanpa koordinasi — menghasilkan retry amplification yang memperparah beban pada downstream.
flowchart TD
subgraph Amplification["❌ Retry Amplification — Setiap Layer Retry Sendiri"]
C1["Client\n3x retry"] --> G1["API Gateway\n3x retry"]
G1 --> S1["Service A\n3x retry"]
S1 --> D1["Downstream\n3×3×3 = 27 calls\nuntuk 1 request user!"]
end
subgraph Best["✅ Best Practice — Retry di Satu Layer Tepat"]
C2["Client\nfail fast"] --> G2["API Gateway\n3x retry ← di sini saja"]
G2 --> S2["Service A\nfail fast"]
S2 --> D2["Downstream\nmaks 3 calls saja"]
endPanduan retry per layer:
| Layer | Retry Cocok Untuk | Catatan |
|---|---|---|
| HTTP Client | Timeout, 5xx dari upstream | Wajib ada idempotency key |
| Message Consumer | Transient processing error | Kombinasikan dengan DLQ |
| Background Worker | External API call yang flaky | Kombinasikan dengan job state persistence |
| Database Client | Deadlock, connection error transient | Sudah built-in di banyak driver |
| gRPC Client | Status UNAVAILABLE, DEADLINE_EXCEEDED | Gunakan retry policy di service config |
Implementasi di Dart/Flutter (Dio Interceptor) #
// Retry transport untuk http.Client — berlaku untuk semua HTTP request
type RetryTransport struct {
base http.RoundTripper
maxRetries int
baseDelay time.Duration
}
func (t *RetryTransport) RoundTrip(req *http.Request) (*http.Response, error) {
retryCount := 0
for {
resp, err := t.base.RoundTrip(req)
if !isRetryable(err, resp) || retryCount >= t.maxRetries {
return resp, err
}
retryCount++
// Exponential backoff dengan full jitter
maxDelayMs := int64(t.baseDelay.Milliseconds()) * int64(1<<uint(retryCount))
jitterMs := rand.Int63n(maxDelayMs)
log.Printf("[RETRY] attempt %d/%d dalam %dms untuk %s",
retryCount, t.maxRetries, jitterMs, req.URL.Path)
time.Sleep(time.Duration(jitterMs) * time.Millisecond)
}
}
func isRetryable(err error, resp *http.Response) bool {
// Network error transient
if err != nil {
return true
}
// HTTP status yang retryable
return resp != nil && (resp.StatusCode == 429 || resp.StatusCode == 500 ||
resp.StatusCode == 502 || resp.StatusCode == 503 || resp.StatusCode == 504)
}
// Setup di main
func setupClient() *http.Client {
return &http.Client{
Transport: &RetryTransport{maxRetries: 3, baseDelay: 200 * time.Millisecond},
}
}
Observability untuk Retry #
Retry yang tidak ter-observe adalah retry yang tidak bisa di-tune. Tanpa metrics, kamu tidak tahu apakah retry configuration sudah optimal atau justru menyembunyikan masalah yang lebih dalam.
// Metrics wajib untuk setiap retry mechanism
func RetryWithMetrics(ctx context.Context, operationName string,
cfg RetryConfig, fn func() error) error {
startTime := time.Now()
totalAttempts := 0
err := RetryWithBackoff(ctx, cfg, func() error {
totalAttempts++
attemptErr := fn()
if attemptErr != nil && totalAttempts > 1 {
metrics.Counter("retry.attempt",
"operation", operationName,
"attempt", strconv.Itoa(totalAttempts),
).Inc()
}
return attemptErr
})
duration := time.Since(startTime)
if err != nil {
// Semua retry habis dan masih gagal
metrics.Counter("retry.exhausted", "operation", operationName).Inc()
metrics.Histogram("retry.duration_on_failure",
"operation", operationName,
).Record(duration.Seconds())
} else if totalAttempts > 1 {
// Berhasil setelah retry — track berapa attempt
metrics.Counter("retry.recovered",
"operation", operationName,
"attempts", strconv.Itoa(totalAttempts),
).Inc()
}
return err
}
Metrics yang wajib dipantau:
| Metrik | Artinya | Alert Jika |
|---|---|---|
retry.attempt count | Berapa kali retry dilakukan | Naik terus → downstream mulai bermasalah |
retry.exhausted count | Retry habis, masih gagal | > 0 → butuh investigasi |
retry.recovered count | Berhasil setelah retry | Perlu untuk menghitung recovery rate |
retry.duration_on_failure | Total waktu termasuk semua retry | Terlalu tinggi → turunkan max attempts |
Anti-Pattern yang Harus Dihindari #
// ✗ Infinite retry — sistem stuck, resource leak
for {
err := callService()
if err == nil { break }
time.Sleep(100 * time.Millisecond)
}
// ✓ Selalu ada MaxAttempts atau total timeout budget
// ✗ Retry semua error tanpa klasifikasi
RetryWithBackoff(ctx, cfg, func() error {
return validateUserInput(req) // 400 tidak akan berubah kalau di-retry!
})
// ✓ Definisikan IsRetryable yang eksplisit
// ✗ Retry berlapis tanpa koordinasi — amplification 3×3×3 = 27 calls
// ✓ Tentukan satu layer yang bertanggung jawab, layer lain fail-fast
// ✗ Tidak ada delay — immediate retry untuk network call
for attempt := 0; attempt < 5; attempt++ {
callExternalService() // flood ke downstream
}
// ✓ Exponential backoff + jitter
// ✗ Retry operasi non-idempotent tanpa idempotency key
RetryWithBackoff(ctx, cfg, func() error {
return chargeCard(userID, amount) // bisa double charge!
})
// ✓ Pastikan idempotency sebelum menambahkan retry
// ✗ Sembunyikan semua error setelah retry habis
if err := RetryWithBackoff(...); err != nil {
log.Error(err)
return nil // ← caller tidak tahu ada masalah!
}
// ✓ Propagate error, biarkan caller memutuskan penanganan selanjutnya
// ✗ Retry setelah context cancelled
for attempt := 1; attempt <= maxAttempts; attempt++ {
fn()
time.Sleep(delay) // tidak cek ctx.Done()!
}
// ✓ Selalu cek ctx.Done() di select sebelum tunggu delay
Checklist Implementasi Retry Strategy #
KONFIGURASI:
□ MaxAttempts atau total timeout budget sudah ditetapkan (tidak infinite)
□ BaseDelay, MaxDelay sudah disesuaikan dengan SLA downstream
□ Full jitter diaktifkan untuk mencegah thundering herd
KLASIFIKASI ERROR:
□ IsRetryable function sudah didefinisikan secara eksplisit
□ 4xx errors (kecuali 429) dikecualikan dari retry
□ context.Canceled dan context.DeadlineExceeded dikecualikan dari retry
IDEMPOTENCY:
□ Semua operasi yang di-retry sudah dipastikan idempotent
□ Idempotency key digunakan untuk operasi non-idempotent
LAYER DESIGN:
□ Tidak ada retry bertumpuk di multiple layer untuk operasi yang sama
□ Layer yang bertanggung jawab retry sudah ditentukan
OBSERVABILITY:
□ retry.attempt, retry.exhausted, retry.recovered di-track sebagai metrics
□ Alert terpasang jika retry.exhausted > threshold
□ Log mencantumkan attempt number, delay, dan error message
TESTING:
□ Test untuk happy path: berhasil di attempt pertama
□ Test untuk retry path: gagal N kali, berhasil di attempt terakhir
□ Test untuk exhausted: gagal semua attempt, DLQ atau final error
□ Test untuk non-retryable: langsung fail fast tanpa retry
Ringkasan #
- Retry Strategy adalah pendekatan sistematis — bukan loop biasa; memiliki aturan jelas tentang kapan, berapa kali, berapa lama, dan untuk error apa retry dilakukan.
- Enam jenis retry: immediate (hindari untuk network), fixed delay (thundering herd risk), exponential backoff (lebih baik), exponential backoff + jitter (best practice), deadline-based (latency-sensitive), kombinasi circuit breaker (mencegah retry yang sia-sia).
- Exponential backoff + full jitter adalah default terbaik — memberi downstream waktu pulih sekaligus menyebar timing retry untuk menghindari thundering herd.
- Klasifikasi error adalah kritis: 4xx (kecuali 429) hampir tidak pernah layak di-retry; 5xx dan timeout biasanya layak; context.Canceled tidak boleh di-retry.
- Idempotency adalah prasyarat mutlak — sebelum menambahkan retry ke operasi apapun, pastikan operasi tersebut aman dijalankan lebih dari sekali; gunakan idempotency key jika perlu.
- Retry amplification terjadi saat setiap layer melakukan retry independen — 3×3×3 = 27 call untuk 1 request user; tetapkan satu layer yang bertanggung jawab.
- Context cancellation harus dihormati — selalu cek
ctx.Done()di antara retry; jangan teruskan setelah context cancelled.- Observability wajib — track retry count, recovery rate, dan exhausted count; tanpa metrics tidak bisa tahu apakah konfigurasi sudah optimal.
- Infinite retry adalah anti-pattern — selalu ada max attempts atau total timeout budget untuk mencegah resource leak dan sistem stuck.
← Sebelumnya: Aspect Oriented Programming Berikutnya: Backoff Strategy →