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

Rate Limiting — 429, Retry-After, 아래의 algorithm

~10 min · auth-security, rate-limiting, 429, retry-after

Level 0HTTP Newbie
0 XP0/46 lessons0/12 achievements
0/120 XP to next level120 XP to go0% complete
"Rate limiting 이 protocol 의 '나가지 말고 느려져' 라는 말 방식. Status code 가 429; 회복 정보가 Retry-After; algorithm 선택이 얼마나 관대할지에 의존."

왜 rate limit

Client 가 얼마나 자주 칠 수 있는지 제한하는 독립 이유 셋:

  • 비용 통제. 각 request 가 CPU, RAM, DB 쿼리, 가능하면 upstream API 호출 (실제 돈) 비용. 무제한 client 가 분 안에 청구 올림.
  • 공평성. 한도 없으면 한 무거운 user 가 다른 사람들 굶김. Rate limit 가 "어느 user 도 분당 X 이상 못 받음" 강제, 모두 위해 capacity 보존.
  • 남용 / DoS 보호. 꽉 loop 에서 retry 하는 잘못 행동하거나 악성 client 가 너를 다운시킬 수 있음. Rate limit 가 "이건 더 이상 정당 사용 아님" 선 그음.

429 + Retry-After 계약

한도 치면 server 가 다음과 함께 429 Too Many Requests 돌려줌:

HTTP/1.1 429 Too Many Requests
Retry-After: 60
Content-Type: application/json

{"error": {"code": "rate_limited", "message": "60초 후 다시 시도."}}

Retry-After 가 초 수나 HTTP-date 가능. 어느 쪽이든 server 의 계약: 이만큼 기다리고 retry. 무시는 적대적 — 대부분 server 가 한도 윈도우 연장하거나 IP 차단으로 non-respecting client 에 대해 강수.

많은 API 가 모든 response (성공이나 실패) 에 rate-limit 힌트 header 도 포함:

  • X-RateLimit-Limit: 100 — cap (윈도우 당).
  • X-RateLimit-Remaining: 47 — 현재 윈도우에 남은 request 수.
  • X-RateLimit-Reset: 1760000000 — 윈도우 reset 되는 Unix timestamp.

잘 동작하는 client 가 이거 추적하고 Remaining 이 영 접근하면 능동적 back off. Anthropic, OpenAI, GitHub, Stripe, Twilio 다 이 header 노출.

흔한 algorithm 네 가지

Fixed Window. 분당 100 request. 분 경계에 윈도우 reset. 구현 단순; "경계의 천둥 무리" 취약 — client 가 reset 직전 100 + 직후 100, 사실상 2초에 200 가능.

Sliding Window Log. 모든 request 의 timestamp 추적; 최근 60초의 request 셈. 부드럽고 정확; scale 에서 메모리 무거움 (request 당 항목 하나).

Sliding Window Counter. Fixed window 를 weighted overlap 과 결합 (현재 윈도우 100% + 이전 윈도우 비례). 근사하지만 싸움; 대부분 production system 이 쓰는 것.

Token Bucket. 각 client 가 token 보유 bucket 가짐. 각 request 가 하나 소비. Token 이 고정 rate 로 refill. 지속 rate 안에서 burst (full bucket) 허용. 가장 유연; Stripe 와 AWS 가 씀.

Header 가 client 한테 가능한 거 알려줌; algorithm 이 정중한 거 결정. X-RateLimit-Remaining surface 가 잘 동작하는 client 가 절대 429 안 치고 self-regulate 가능. 429 + Retry-After 가 안 하는 client 위한 안전망. 둘 다 필요; 어느 것도 단독으론 안 충분.

한도 적용할 곳

세밀도 층 셋, 보통 결합:

  • IP 당 — 미인증 남용에 대한 보호. 거침; NAT 뒤 user 처벌 가능.
  • API key / user 당 — 계정 당 공평 한도. 가장 흔함.
  • Endpoint 당 — 비싼 endpoint (search, ML inference) 가 싼 거보다 더 빡빡한 한도.

결합: "globally API key 당 100 req/min, /search 엔 10 req/min 만." 가능하면 header 통해 둘 다 surface.

분산 rate limiting

Service 가 여러 instance 에서 돌면 in-memory rate counter 가 각 instance 한테 자기 quota 줌 — user 가 5 다른 replica 쳐서 의도 한도의 5x 칠 수 있음. 표준 fix: Redis 를 공유 counter 로 (key 당 atomic INCR + EXPIRE). Request 당 Redis round-trip 추가; 보통 가치 있음.

매우 높은 트래픽 엔 sample 기반 접근 (N request 중 하나 세고 곱함) 이 근사 비용으로 Redis 부하 감소.

cwkPippa 의 rate limiting 현실

cwkPippa 가 자기 API 현재 rate-limit 안 함 — 인간 client 가 최대 둘 (아빠 + WebUI 통한 피파). Brain adapter 가 upstream API (Anthropic, OpenAI) 에서 rate limit 마주치고, fallback chain (Codex → Claude → Gemini) 이 429 에 brain 전환으로 처리. Gemini OAuth quota visible-fallback (feedback_no_auto_gemini_fallback 참조) 가 canonical 예: Gemini 무료 quota 가 429 치면 system 이 sticky toast 경고 가진 paid API key 로 자동 flip. 진짜 production: cwkPippa webhook 수신기를 제 3자한테 ship 하면 Stripe 가 rate-limit; 그게 API-key 당 한도 등장하는 날.

Code

slowapi 가진 FastAPI rate limit — endpoint 당 빡빡함·python
# FastAPI — slowapi (표준 라이브러리) 쓴 token-bucket rate limit
from fastapi import FastAPI, Request
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.errors import RateLimitExceeded
from slowapi.util import get_remote_address

app = FastAPI()
limiter = Limiter(key_func=get_remote_address)  # client IP 당 rate-limit
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.get('/api/cheap')
@limiter.limit('100/minute')
async def cheap_endpoint(request: Request):
    return {'ok': True}

@app.get('/api/expensive')
@limiter.limit('10/minute')  # 비싼 endpoint 에 더 빡빡 한도
async def expensive_endpoint(request: Request):
    return {'ok': True}

# 한도 치면 발생:
# HTTP/1.1 429 Too Many Requests
# Retry-After: 60
# X-RateLimit-Limit: 10
# X-RateLimit-Remaining: 0
# X-RateLimit-Reset: 1760000000
# {"error":"Rate limit exceeded: 10 per 1 minute"}
Client: Retry-After 존중 + Remaining < 5 일 때 사전 느려짐·python
# Client 쪽 — Retry-After 와 X-RateLimit-Remaining 존중
import httpx, time

class RateAwareClient:
    def __init__(self, base_url: str):
        self.client = httpx.Client(base_url=base_url)

    def request(self, method: str, path: str, **kwargs) -> httpx.Response:
        for attempt in range(3):
            resp = self.client.request(method, path, **kwargs)
            if resp.status_code == 429:
                wait = int(resp.headers.get('Retry-After', 5))
                time.sleep(wait)
                continue
            # 한도 가까우면 사전 backoff
            remaining = int(resp.headers.get('X-RateLimit-Remaining', 100))
            if remaining < 5:
                reset_ts = int(resp.headers.get('X-RateLimit-Reset', time.time() + 60))
                wait = max(0, reset_ts - int(time.time()))
                if wait > 0:
                    time.sleep(min(wait, 1))  # gentle 느려짐
            return resp
        return resp  # 포기; 429 caller 한테 surface
분산 rate limit — replica 간 공유 counter 로 Redis·python
# Redis 통한 분산 rate limit — atomic check-and-decrement 가진 token bucket
import time, redis

rdb = redis.Redis()

def is_allowed(client_id: str, limit: int = 100, window_s: int = 60) -> tuple[bool, int]:
    """Returns (allowed, remaining). Atomicity 위해 Redis INCR + EXPIRE 씀."""
    now = int(time.time())
    window_start = now - (now % window_s)
    key = f'rl:{client_id}:{window_start}'

    pipe = rdb.pipeline()
    pipe.incr(key)
    pipe.expire(key, window_s * 2)  # cross-window request 위한 여유
    count, _ = pipe.execute()

    remaining = max(0, limit - count)
    return count <= limit, remaining

# FastAPI middleware 에서:
# allowed, remaining = is_allowed(client_id)
# if not allowed:
#     return JSONResponse(429, content={'error': 'rate_limited'},
#                         headers={'Retry-After': str(window_s),
#                                  'X-RateLimit-Remaining': '0'})

External links

Exercise

FastAPI endpoint 하나 (/api/expensive) 에 분당 5 한도로 rate limit 추가. 모든 response 에 X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset 노출, 429 에 Retry-After. 그 다음 20 request loop 하는 client 써, request 6 쯤 429 칠 거 기대, Retry-After 읽고, sleep, 20 다 성공할 때까지 retry. 보너스: in-memory counter 를 Redis 로 교체하고 parallel client instance 둘이 같은 한도 공유 검증 (Redis 없으면 각 instance 가 자기 분당 5 가져서 실제 capacity 두 배).
Hint
slowapi 가 FastAPI 쪽 처리; header 에 대해선 rate-limit 체크 후 도는 custom middleware 에서 계산 가능. Client: 429 잡고, int(resp.headers['Retry-After']) 읽고, time.sleep(), loop. Redis 보너스가 production-shape test — 분산 counter 없으면 service scale out 이 replica 수만큼 effective rate limit 곱해, 팀이 처음 horizontal scale 할 때 무는 미묘 production gotcha.

Progress

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

댓글 0

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

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