Retry & Exponential Backoff
Tại Sao Quan Trọng
Retry cấu hình sai gây:
- Cascade failure — backoff quá ngắn → retry storm → hơn nữa down
- Silent drops — max attempts vượt quá trước handler có cơ hội retry
- Resource waste — exponential backoff không cấu hình → burst retries
Bạn cần hiểu:
- Retry được trigger khi nào (HTTP status, timeout)
- Exponential backoff công thức
- Max attempts, min/max backoff, doubling factor
- SLA: task có retry bao lâu trước khi dropped
Retry Trigger Conditions
Cloud Tasks retry task nếu:
1. HTTP Status Code Không 200-299
200 OK → SUCCESS (task deleted)
201 Created → SUCCESS
299 Whatever → SUCCESS
300 Redirect → RETRY (suspicious, shouldn't happen for task)
400 Bad Request → RETRY (might be transient)
401 Unauthorized → RETRY (token might expire and refresh)
500 Server Error → RETRY (temporary server issue)
503 Service Unavailable → RETRY (backoff kicked in)
BUT:
- Response không đến (network partition) → TIMEOUT → RETRY2. Timeout
Handler không gửi response trong timeout duration (App Engine standard: 10 minutes).
Dispatch sent: 10:00:00
Timeout configured: 10 minutes
If no response by 10:10:00 → assume FAILED → RETRY3. Handler Crash / Crash Loop
Handler nhận request
Handler crashed mid-processing
No response sent
Timeout → RETRYExponential Backoff Formula
Config Parameters
go
type RetryConfig struct {
MaxAttempts int32 // Default: 100
MinBackoff time.Duration // Default: 100ms
MaxBackoff time.Duration // Default: 3,600s (1 hour)
MaxDoublings int32 // Default: 16
}Formula
delay_i = min(
max_backoff,
min_backoff * (2 ^ min(i, max_doublings))
) + random_jitterDimana:
i= retry attempt number (0-indexed)2^i= exponential growthmax_doublings= caps exponential (prevents infinite growth)random_jitter= ±10% random, prevent thundering herd
Default Config Example
min_backoff = 100ms
max_backoff = 3600s (1 hour)
max_doublings = 16
Attempt 0: delay = 100ms
Attempt 1: delay = 100ms * 2^1 = 200ms
Attempt 2: delay = 100ms * 2^2 = 400ms
Attempt 3: delay = 100ms * 2^3 = 800ms
Attempt 4: delay = 100ms * 2^4 = 1.6s
Attempt 5: delay = 100ms * 2^5 = 3.2s
...
Attempt 16: delay = 100ms * 2^16 = 6,553.6s (~1.8 hours) ← capped!
Attempt 17+: delay = 3,600s (1 hour) ← flat at max_backoffTotal retry window:
With max_attempts = 100:
Retries scheduled over ~100 hours (4+ days)
If task succeeds on attempt 50, it was retried ~25 timesBackoff Curve — Visualization
Delay (seconds)
3600 ─────────────────────────────────
╱─────────────────────────────┐
1000 │ │
│ ╱───────────────────┐ │
100 │ ╱ │ │
│ ╱ │ │
1 ├┘ │ │
│ 0 5 10 15 20 25 30 │ │ 50 100
└────────────────────────────────────── Attempt
max_doublings=16 capped →Characteristics:
- First 16 attempts: exponential growth (2x each)
- After attempt 16: flat at
max_backoff(1 hour) - Total duration: ~4+ days for 100 attempts
Jitter — Preventing Thundering Herd on Retry
Problem:
All 1,000 tasks fail at 10:00:00
Without jitter: all 1,000 retry at 10:00:100ms
Result: thundering herd, handler overloadWith jitter:
All 1,000 tasks fail at 10:00:00
Each scheduled retry with ±10% random delay:
Task 1: retry at 10:00:090ms
Task 2: retry at 10:00:095ms
Task 3: retry at 10:00:105ms
...
Task 1000: retry at 10:00:110ms
Result: spread retries over ~20ms window (smooth, no herd)Max Attempts Semantics
Default: 100 Attempts
Attempt 1: immediate
Attempt 2: 200ms later
Attempt 3: 400ms later
...
Attempt 100: ~3 days later
Attempt 101: DROPPED (max reached)If task not succeed after 100 attempts:
- Task is silently dropped (unless DLQ configured)
- No alert, no notification
- This is why monitoring is critical
Custom Config
go
client.UpdateQueue(ctx, &Queue{
Name: "projects/X/locations/Y/queues/critical",
RetryConfig: &RetryConfig{
MaxAttempts: 50, // Shorter retry window
MinBackoff: 1*time.Second,
MaxBackoff: 60*time.Second, // 1 minute, not 1 hour
MaxDoublings: 10, // Cap exponential sooner
},
})
// Now: retry window ~30 minutes (not 4 days)Failure Modes & Boundaries
Mode 1: Transient Failure (Network Blip)
Attempt 1 (10:00:00): Network timeout
Attempt 2 (10:00:00.2s): Network recovered
→ SUCCESS
Total duration: 200msMode 2: Handler Bug (Slow Recovery)
Attempt 1 (10:00:00): Handler crashed
Attempt 2 (10:00:00.2s): Still crashed
Attempt 3 (10:00:00.4s): Still crashed
...
Attempt 10 (10:00:51.2s): Handler recovered, restarted
→ SUCCESS
Total duration: 51 secondsMode 3: Permanent Failure (No Recovery)
Attempt 1 (10:00:00): Database corrupt, data lost forever
Attempt 2 (10:00:00.2s): Still corrupt
...
Attempt 100 (72 hours later): Still corrupt
→ DROPPED (max attempts reached)
Task is gone, no retry anymoreMode 4: Cascade Failure
Attempt 1 (10:00:00): Handler normal
→ Fails due to resource contention (DB locked)
→ Throws 500 error
Attempt 2 (10:00:00.2s): Retry
→ DB still locked
→ Throws 500 error
Retry storm: exponential backoff eventually kicks in
But if:
- min_backoff too short
- max_doublings too small
- max_backoff too small
Result: Retries still flood handler, preventing recoveryConfiguring Backoff for Different Workloads
Pattern 1: Strict SLO (Critical Path)
go
// E.g., payment processing
queue := &Queue{
RetryConfig: &RetryConfig{
MaxAttempts: 10, // Only 10 retries
MinBackoff: 100*time.Millisecond,
MaxBackoff: 10*time.Second, // Cap at 10 sec
MaxDoublings: 5, // Exponential only 5x
},
}
// Retry window: ~30 seconds total
// If fail after 30s, incident (investigate immediately)Pattern 2: Batch Processing (Relaxed SLO)
go
// E.g., daily report generation
queue := &Queue{
RetryConfig: &RetryConfig{
MaxAttempts: 100, // Full retries
MinBackoff: 1*time.Second,
MaxBackoff: 1*time.Hour, // Full 1 hour
MaxDoublings: 16, // Full exponential
},
}
// Retry window: ~4 days
// Task can take time, eventually succeedsPattern 3: Transient-Heavy (Quick Recovery)
go
// E.g., cache invalidation
queue := &Queue{
RetryConfig: &RetryConfig{
MaxAttempts: 20, // Medium retries
MinBackoff: 10*time.Millisecond,
MaxBackoff: 1*time.Second, // Short max
MaxDoublings: 8, // Quick plateau
},
}
// Retry window: ~5 seconds
// Assumes transient issues resolve quicklyObservability & Monitoring
What to Track
retry_rate (%)
→ % of tasks that needed retry
→ Alert if > threshold (e.g., > 5%)
max_attempts_reached
→ Count of tasks dropped due to max attempts
→ Should be 0 for healthy system
retry_latency_p99
→ How long between attempt N and N+1
→ Verify backoff working as expected
task_age_seconds
→ Age of oldest pending task
→ If very old, something stuck
attempt_distribution
→ Histogram: how many at attempt 1, 2, 3, ..., 100
→ Should be skewed towards low attempts
→ If skewed towards 100, many permanent failuresAlert Rules
Alert: max_attempts_reached > 0
→ Permanent failures happening
→ Action: Debug and fix handler / data integrity
Alert: retry_rate > 10% for > 5min
→ Handler instability
→ Action: Investigate handler, logs, dependencies
Alert: task_age_seconds > 3600 (1 hour)
→ Queue stuck
→ Action: Check if queue paused, if rate limits hit, handler down
Alert: attempt_distribution tail heavy (> 50% at attempt 20+)
→ Slow recovery from transient failure
→ Action: Shorten max_backoff? Increase rate limits?Manual Retry & Force Run
Force Run Task (Immediate Execution)
go
// Task stuck in retry, you want to retry now (not wait for backoff)
client.RunTask(ctx, &RunTaskRequest{
Name: "projects/X/locations/Y/queues/Z/tasks/my-task",
})
// Task immediately dispatched (bypass exponential backoff)Quota: 60 ForceRun per minute per region.
Practical Use Case
Handler bug discovered, fix deployed 10:30:00
But failing tasks are scheduled to retry at:
10:30:30 (attempt 2)
10:31:00 (attempt 3)
10:32:00 (attempt 4)
Instead of wait, ForceRun:
```bash
gcloud tasks run --queue=my-queue --location=us-central1 my-taskTask retried immediately (if still failing).
Dead Letter Queue Integration (Preview)
Scenario: Task reaches max attempts, dropped normally.
With DLQ: Task sent to separate "dead letter" queue for analysis.
go
queue := &Queue{
Name: "projects/X/locations/Y/queues/main",
DeadLetterConfig: &DeadLetterConfig{
DeadLetterQueue: "projects/X/locations/Y/queues/dead-letter",
},
}
// If task fails max_attempts in "main" queue
// → Task automatically created in "dead-letter" queue
// → Operator manually investigates, fixes data, re-enqueues