~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'})
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.