C.W.K.
Stream
Lesson 05 of 06 · published

Client 설계 — Retry, Backoff, Circuit Breaker

~11 min · production, retry, backoff, circuit-breaker

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Production HTTP client 가 `httpx.get(url)` 아냐. Retry-with-backoff (transient 실패) 로 감싸진 `httpx.get(url)`, Retry-After 존중 (rate limit), idempotency 인식 (POST 맹목 retry 안 함), circuit-breaker 보호 (flapping upstream 이 service 전체 죽이지 않게) 됨."

Production client resilience 의 세 층

1. Transient 실패에 retry. 네트워크 glitch, 5xx error, 429 rate limit — 다 일시적. Exponential backoff 로 retry: 1s, 2s, 4s, 8s, 어느 max 에 cap. Client 천둥 무리가 lockstep 으로 retry 안 하도록 jitter 추가.

2. Idempotency 인식. Idempotent (GET, HEAD, OPTIONS, PUT, DELETE) method 만 자동 retry. POST/PATCH 는 안전하려면 Idempotency-Key header 필요; 없으면 실패하고 bubble up. Retry 결정이 method 반영 필수.

3. 지속 실패에 circuit breaker. Upstream service 가 근본적으로 다운 (10+ 연속 5xx, latency spike) 이면 cooldown 기간 동안 호출 멈춤. Circuit "open"; 후속 request 에 fail fast (cached 돌려줌, error 돌려줌); cooldown 후 probe 하나 보냄; 동작하면 "close" 하고 재개.

Exponential backoff 공식

표준 exponential backoff: delay = min(base * 2^attempt, max_delay). 동기화 피하려고 jitter 와: delay = random_uniform(0, base * 2^attempt) (full jitter) 혹은 delay = base * 2^attempt + random(0, base) (equal jitter).

# Attempt 0:  ~1s
# Attempt 1:  ~2s
# Attempt 2:  ~4s
# Attempt 3:  ~8s
# Attempt 4:  ~16s (max_delay 에 cap)
# 총 경과: 최종 실패 전 ~31s

Jitter 비협상. 없으면 10,000 client 가 다 500 돌려주는 endpoint 치고 같은 순간 retry 시작하면, 다 unison 으로 retry — upstream 이 과부하 유지 보장. Random delay 가 desynchronize.

Retry 결정 매트릭스

ResponseRetry?주의
네트워크 에러 (timeout, connection refused)Yes (idempotent 이면)Transport-level; idempotent method retry 안전
2xxNo성공; 끝
3xxRedirect followRetry 아님; protocol-level navigation
4xx (408, 429 제외)NoClient error; retry 도움 안 됨
408 Request TimeoutYes (idempotent 이면)Transient server-쪽 timeout
429 Too Many RequestsYes, Retry-After 후Header 존중
5xxYes, backoff 와Server 이슈; 해결 가능

Circuit breaker 상태

  • CLOSED — 정상. Request 흐름; 실패 셈.
  • OPEN — 최근 실패 너무 많음. Request 가 upstream 호출 없이 short-circuit. Cached 값 돌려줌, 나중 큐, 빠른 error 돌려줌.
  • HALF-OPEN — Cooldown 경과. Probe request 하나 보냄. 성공이면 CLOSED 로 전환. 실패면 OPEN 으로 돌아감.

튜닝: 전형적 threshold 가 마지막 20 request 중 50% 실패 rate 가 circuit open; HALF-OPEN 전 30-60초 cooldown. Upstream 당 튜닝; payment API 가 이미지 service 보다 더 빡빡 한도 받을 만.

Retry + backoff 가 transient 실패 처리; circuit breaker 가 지속 실패 처리. Retry 없으면 모든 transient blip 이 error 로 surface. Circuit breaker 없으면 지속 upstream 실패가 네 service 로 cascade. 둘 다 production 에 필수; 라이브러리 (tenacity, polly, resilience4j) 가 one-decorator 쉽게 만듦.

cwkPippa 의 resilience 패턴

cwkPippa 의 brain adapter 가 upstream LLM 호출 (Claude, OpenAI, Gemini) 를 Retry-After 존중하면서 5xx 와 429 에 retry 로 감쌈. Brain fallback chain (Codex → Claude → Gemini) 이 수동 circuit breaker 처럼 작용 — 한 provider 의 5xx rate spike 면 heartbeat scheduler 가 다음 round 위해 다음 brain 으로 swap. 공식 Hystrix-스타일 breaker 없음; fallback chain 이 사실상 가난한 사람 버전. 적절 scale 의 production 이 진짜 circuit breaker 라이브러리에서 이득 보겠지만, 두-user 볼륨엔 수동 fallback 이 충분.

Code

Tenacity: exponential backoff + jitter + 특정 예외 type 에 retry·python
# Backoff 와 jitter 가진 retry — tenacity 라이브러리
from tenacity import retry, wait_exponential_jitter, stop_after_attempt, retry_if_exception_type
import httpx

# Idempotent method — transient 실패에 retry 안전
@retry(
    wait=wait_exponential_jitter(initial=1, max=30, jitter=1),
    stop=stop_after_attempt(5),
    retry=retry_if_exception_type((httpx.TransportError, httpx.HTTPStatusError)),
)
def fetch_user(uid: str) -> dict:
    resp = httpx.get(f'https://api.example.com/users/{uid}')
    if resp.status_code >= 500:
        resp.raise_for_status()  # retry trigger
    if resp.status_code == 429:
        # Retry 전 Retry-After 존중
        import time
        wait = int(resp.headers.get('Retry-After', 5))
        time.sleep(wait)
        raise httpx.HTTPStatusError('rate limited', request=resp.request, response=resp)
    resp.raise_for_status()
    return resp.json()
pybreaker: upstream HTTP 호출 주변 circuit-breaker·python
# 단순 circuit breaker — pybreaker 라이브러리
import pybreaker
import httpx

breaker = pybreaker.CircuitBreaker(
    fail_max=5,           # 5 연속 실패 후 OPEN
    reset_timeout=60,     # HALF-OPEN probe 전 60s cooldown
    exclude=[httpx.HTTPStatusError],  # 4xx 를 실패로 안 셈 (client 이슈)
)

@breaker
def call_upstream(url: str) -> dict:
    resp = httpx.get(url, timeout=10.0)
    resp.raise_for_status()
    return resp.json()

# 사용
try:
    data = call_upstream('https://api.example.com/users/42')
except pybreaker.CircuitBreakerError:
    # Circuit 이 OPEN — fail fast, cached 돌려줌, 나중 큐
    data = cached_or_fallback()
except httpx.HTTPError as e:
    # Upstream error 가 circuit threshold 향해 카운트
    raise
Production-shape: retry + backoff + jitter + idempotency + Retry-After·python
# Idempotency 인식 retry decorator (retry + idempotency 체크 결합)
import httpx
import time, random

IDEMPOTENT_METHODS = {'GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE'}

def call_with_resilience(method: str, url: str, idempotency_key: str | None = None, **kwargs):
    """Retry + backoff + idempotency 인식 + Retry-After 존중 결합."""
    method = method.upper()
    # POST/PATCH 는 caller 가 Idempotency-Key 제공할 때만 retry
    can_retry = method in IDEMPOTENT_METHODS or idempotency_key is not None
    if idempotency_key:
        kwargs.setdefault('headers', {})['Idempotency-Key'] = idempotency_key

    max_attempts = 5 if can_retry else 1
    for attempt in range(max_attempts):
        try:
            with httpx.Client(timeout=10.0) as c:
                resp = c.request(method, url, **kwargs)
            if resp.status_code == 429:
                wait = int(resp.headers.get('Retry-After', 1))
                time.sleep(wait)
                continue
            if 500 <= resp.status_code < 600:
                if not can_retry:
                    return resp
                wait = min(2 ** attempt, 30) + random.random()
                time.sleep(wait)
                continue
            return resp
        except httpx.TransportError:
            if not can_retry:
                raise
            wait = min(2 ** attempt, 30) + random.random()
            time.sleep(wait)
    return resp  # 모든 시도 소진

External links

Exercise

실제 HTTP client 호출 (가진 어느 API) 을 5xx 와 TransportError 에 retry+backoff+jitter 위해 tenacity 로 감싸. 일부러 깨진 URL (예: 사용 안 한 localhost 포트) 가리키게 해서 test 하고 exponential backoff (~1s, 2s, 4s 시도 간 gap) 검증. 그 다음 fail_max=3, reset_timeout=10 가진 pybreaker 를 같은 호출 주변 추가. 3 연속 실패 후 breaker 가 OPEN 해야 하고 후속 호출이 네트워크 안 치고 CircuitBreakerError raise 해야. Cooldown 위해 10s 대기, URL fix, 성공 probe 하나가 CLOSED 로 전환하는 거 검증.
Hint
Tenacity 의 wait_exponential_jitter 가 수학 처리; decorator 만 함. Backoff 검증: 각 시도 timestamp log 하고 delta 계산 — 대략 두 배 해야. pybreaker 에 3 실패 후 다음 7-10 호출 (reset_timeout 안) 이 거의 즉시 (네트워크 없는 CircuitBreakerError). State 전환 CLOSED → OPEN → HALF-OPEN → CLOSED 가 canonical 패턴; 모든 production system 이 쓰는 같은 primitive 만드는 것.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.