~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 결정 매트릭스
Response
Retry?
주의
네트워크 에러 (timeout, connection refused)
Yes (idempotent 이면)
Transport-level; idempotent method retry 안전
2xx
No
성공; 끝
3xx
Redirect follow
Retry 아님; protocol-level navigation
4xx (408, 429 제외)
No
Client error; retry 도움 안 됨
408 Request Timeout
Yes (idempotent 이면)
Transient server-쪽 timeout
429 Too Many Requests
Yes, Retry-After 후
Header 존중
5xx
Yes, 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
실제 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.