[Concurrency] Retry + Exponential Backoff + Jitter
정의
Retry with Exponential Backoff + Jitter = 실패 시 점점 긴 간격으로 재시도, 랜덤 분산. 분산 시스템의 thundering herd / retry storm 방지의 표준.
4가지 전략
flowchart LR
Linear["Linear<br/>1s, 2s, 3s, 4s, ..."]
Exp["Exponential<br/>1s, 2s, 4s, 8s, 16s, ..."]
ExpJ["Exp + Jitter<br/>1s + random, 4s + random, ..."]
DecorJ["Decorrelated Jitter<br/>AWS 권장"]
Exponential
sleep = base * (2 ** attempt)
# attempt 0: 1s, 1: 2s, 2: 4s, 3: 8s, ...
문제: 모든 클라이언트가 같은 시각에 재시도 → thundering herd.
Exponential + Full Jitter
sleep = random.uniform(0, base * (2 ** attempt))
완전 분산. AWS / Stripe 표준.
Decorrelated Jitter
sleep = random.uniform(base, prev_sleep * 3)
과거 sleep 의 함수. 완전 무작위 + 점진 증가. 평균적으로 가장 짧은 retry 시간.
재시도 상태 머신
sequenceDiagram
autonumber
participant C as Client
participant S as Service
C->>S: 1차 요청
S-->>C: 503 Unavailable
Note over C: wait 1s + jitter
C->>S: 2차 재시도
S-->>C: 503 Unavailable
Note over C: wait 2s + jitter
C->>S: 3차 재시도
S-->>C: 200 OK
Note over C: 성공, 응답 반환
각 retry 사이에 sleep 을 두고, sleep 은 attempt 가 늘어날수록 exponentially 커진다. jitter 로 동일 시각 재시도를 분산한다.
비교 시뮬레이션
Retry 가능한 에러만 retry
flowchart TD
E[에러 종류]
E -->|5xx, timeout, conn refused| R[Retry OK]
E -->|"4xx (400, 401, 403, 404)"| NoR[Retry 금지]
E -->|429 Too Many Requests| Special[Retry-After 헤더 따름]
E -->|"409 Conflict (idempotent 아니면)"| NoR
E -->|503 + Retry-After| Special
IMPORTANT
4xx 는 클라이언트 오류. retry 해도 같은 결과. 5xx / network 만 retry 가치.
Idempotency 와 짝
flowchart LR
Retry --> Need[Idempotent operation 필수]
Need --> Idem[Idempotency-Key 또는<br/>at-most-once 보장]
비-멱등 (POST 결제) 은 retry 가 중복 처리 위험. idempotency-keys 가 짝.
상한 설정
| 설정 | 권장 |
|---|---|
| Max attempts | 3-5 |
| Max total time | 30-60s |
| Per-attempt timeout | 5-10s |
| Max sleep | 16-32s |
무한 retry = 사용자 영원히 대기. 빠른 deadline + fallback.
Deadline 전파
분산 시스템에서는 클라이언트가 설정한 deadline 을 모든 하위 서비스로 전파해야 한다. 그렇지 않으면 클라이언트는 이미 timeout 되었는데 하위 서비스는 계속 retry 한다.
import grpc
from datetime import datetime, timedelta
# gRPC: deadline 을 context 로 전파
with grpc.insecure_channel("service:50051") as channel:
stub = ServiceStub(channel)
deadline = datetime.utcnow() + timedelta(seconds=10)
# timeout 이 남은 시간으로 자동 설정
resp = stub.CallMethod(request, timeout=10.0)
HTTP 에서는 X-Request-Deadline 같은 커스텀 헤더 또는 X-B3-Deadline 로 전파한다.
CAUTION
deadline 전파 없이 retry 하면 zombie retry 가 발생한다. 클라이언트는 504 를 받고 떠났는데 서버 내부에서는 수 분간 재시도가 계속된다. 자원 낭비 + 부하 증폭.
구현 예시
Python: tenacity
from tenacity import (
retry,
stop_after_attempt,
wait_exponential,
wait_random,
retry_if_exception_type,
)
import requests
@retry(
stop=stop_after_attempt(5),
wait=wait_exponential(multiplier=1, min=1, max=16) + wait_random(0, 2),
retry=retry_if_exception_type((requests.ConnectionError, requests.Timeout)),
reraise=True,
)
def call_api(url: str) -> dict:
resp = requests.get(url, timeout=5)
resp.raise_for_status()
return resp.json()
Java: Resilience4j
RetryConfig config = RetryConfig.custom()
.maxAttempts(5)
.intervalFunction(IntervalFunction.ofExponentialRandomBackoff(500, 2.0, 0.5))
.retryOnException(e -> e instanceof HttpServerErrorException)
.build();
Retry retry = Retry.of("api-call", config);
Supplier<String> decorated = Retry.decorateSupplier(retry, () -> callDownstream());
Try.ofSupplier(decorated).recover(ex -> fallback()).get();
Go: backoff 라이브러리
import "github.com/cenkalti/backoff/v4"
b := backoff.NewExponentialBackOff()
b.InitialInterval = 1 * time.Second
b.MaxInterval = 16 * time.Second
b.MaxElapsedTime = 60 * time.Second
err := backoff.RetryNotify(
func() error {
return callAPI(ctx)
},
backoff.WithContext(b, ctx),
func(err error, d time.Duration) {
log.Printf("retry after %v: %v", d, err)
},
)
Hedged Requests
Retry 와 다른 전략: 첫 요청이 일정 시간 내에 응답 안 오면 두 번째 요청을 병렬로 보내고, 먼저 오는 응답을 사용. 실패 없이 tail latency 를 낮추는 패턴.
t=0: → request 1
t=50ms → (응답 없음) → request 2 병렬 시작
t=80ms ← response 2 도착 → response 1 취소
import asyncio
async def hedged_call(url: str, hedge_delay=0.05) -> dict:
async with aiohttp.ClientSession() as session:
task1 = asyncio.create_task(session.get(url))
await asyncio.sleep(hedge_delay)
task2 = asyncio.create_task(session.get(url))
done, pending = await asyncio.wait(
[task1, task2], return_when=asyncio.FIRST_COMPLETED
)
for t in pending:
t.cancel()
return await done.pop()
IMPORTANT
Hedging 은 non-idempotent 요청 (POST 결제 등) 에는 금지. 멱등 읽기 (GET, SELECT) 에만 사용.
Retry-After 헤더
HTTP/1.1 429 Too Many Requests
Retry-After: 30
또는
Retry-After: Wed, 25 Jun 2026 12:30:00 GMT
서버가 client 에게 얼마 후 다시 시도 알림. 클라이언트는 반드시 존중.
SDK / Library
| 도구 | 언어 |
|---|---|
| AWS SDK | 모든 언어 (자동 retry) |
| Polly | .NET |
| Resilience4j Retry | Java |
| tenacity | Python |
| node-fetch + retry | Node |
| backoff (Go) | Go |
Retry Budget
flowchart LR
Calls[전체 호출 수] --> Budget[Retry 예산: 20%]
Budget --> Limit[1000 호출 → 최대 200 retry]
Limit --> Reject[초과 시 fast fail]
한 service 의 retry 가 전체 호출의 큰 비중 이면 retry storm. 예산 한도 로 보호. gRPC 의 retry budget, Envoy retry policy.
흔한 함정
WARNING
- Jitter 없음 = thundering herd. 전체 fleet 동시 retry.
- 모든 에러 retry = 4xx 도 retry → 의미 없는 부하.
- Idempotent 아닌데 retry = 결제 중복. Idempotency-Key 필수.
- Max attempt 무한 = 사용자 영원히 대기. timeout + fallback.
- Nested retry = 계층마다 retry (client → SDK → SDK 내부) → retry × retry 폭증. 한 곳에서만.
- Deadline 미전파 = 클라이언트 timeout 후에도 하위 서비스가 계속 재시도.
관련 위키
- circuit-breaker
- idempotency-keys
- backpressure
- rate-limiting
- api-gateway, gateway 레벨 retry 정책 설정
이 글의 용어 (5개)
- [Concurrency] Backpressure: 흐름 제어로 시스템 보호concurrency
- 정의 Backpressure = 생산자가 소비자 속도에 맞춰 자기 속도 조절. 분산 시스템의 cascade failure 방지. [!IMPORTANT] Backpressure 의…
- [Concurrency] Circuit Breaker: cascade failure 방어concurrency
- 정의 Circuit Breaker = 전기 회로의 차단기처럼, 백엔드 다운 / 느림 시 호출 자체를 차단 → 빠른 실패 + 백엔드 회복 시간. 와 함께 cascade failur…
- [Concurrency] Rate Limiting: token bucket, sliding windowconcurrency
- 정의 Rate Limiting = 시간당 요청 수 제한. API 보호, fair use, 비용 제어, DDoS 완화. 5가지 알고리즘 1. Fixed Window - 단순. - …
- [Pattern] API Gateway: BFF, 라우팅, auth, rate limitdistributed-systems
- 정의 API Gateway = 클라이언트와 마이크로서비스 사이의 단일 진입점. 라우팅, auth, rate limit, transformation, monitoring 의 cro…
- [Pattern] Idempotency Keys: 중복 요청 안전 처리distributed-systems
- 정의 Idempotency = 같은 요청을 N번 보내도 결과가 1번과 동일. 분산 시스템 / 결제 / API 의 안전망. [!IMPORTANT] 네트워크는 항상 timeout /…
💬 댓글