본문으로 건너뛰기
김신건의 로그

[Concurrency] Rate Limiting: token bucket, sliding window

· 수정 · 📖 약 2분 · 727자/단어 #rate-limit #throttling #concurrency #api #backend
Rate Limiting, Token Bucket, Leaky Bucket, Sliding Window, Fixed Window, GCRA, 429 Too Many Requests

정의

Rate Limiting = 시간당 요청 수 제한. API 보호, fair use, 비용 제어, DDoS 완화.

5가지 알고리즘

flowchart LR
    A[Fixed Window]
    B[Sliding Log]
    C[Sliding Window Counter]
    D[Token Bucket]
    E[Leaky Bucket]
    F[GCRA]

1. Fixed Window

[00:00 ~ 00:01] count = 100 (한도)
[00:01 ~ 00:02] count 리셋 → 100
key = f"rl:{user}:{minute_bucket}"
count = redis.incr(key)
redis.expire(key, 60)
if count > 100: deny()
  • 단순.
  • 경계 burst: 00:00:59 에 100 + 00:01:00 에 100 = 2초 안에 200.

2. Sliding Log

모든 요청의 timestamp 저장 → 윈도우 안 카운트.

key = f"rl:log:{user}"
now = time.time()
redis.zadd(key, { req_id: now })
redis.zremrangebyscore(key, 0, now - 60)
count = redis.zcard(key)
if count > 100: deny()
  • 정확.
  • 메모리 큼 (요청마다 entry).

3. Sliding Window Counter

Fixed Window + 이전 window 비율.

current_count + previous_count * (1 - elapsed_in_current/window)
  • 근사.
  • 적은 메모리 + Fixed 의 burst 완화.

4. Token Bucket

flowchart LR
    Refill[1초마다 토큰 N개] --> Bucket[(Bucket: max 100)]
    Bucket -->|토큰 1개 소비| Request[요청 통과]
    Bucket -->|토큰 없음| Deny[429]
  • Burst 허용 (bucket 차있을 때).
  • 평균 rate 보장.
  • AWS / GitHub / Discord API 표준.

5. Leaky Bucket

flowchart TB
    Req[요청] -->|채움| B[Bucket]
    B -->|일정 rate 로 소비| Output[처리]
    B -.넘침.-> Drop[Drop]
  • 완전 평탄화. burst 흡수 + 일정 출력.
  • queue 기반.

6. GCRA (Generic Cell Rate Algorithm)

TAT (Theoretical Arrival Time) 계산
요청 시각이 TAT 보다 충분히 앞이면 허용
  • Token Bucket 수학적 변형.
  • 단일 변수 로 표현. Redis 1 명령 으로 가능.
  • Cloudflare, Stripe 가 사용.

비교 매트릭스

알고리즘메모리정확도Burst구현
Fixed Window적음낮음 (경계 burst)가장 단순
Sliding Log정확없음Sorted Set
Sliding Window Counter적음보통작음2 counter
Token Bucket적음좋음허용간단
Leaky Bucket중간좋음평탄Queue
GCRA최소 (단일 변수)정확가능수학적

분산 환경 구현 (Redis)

# Token Bucket (Lua atomic)
TOKEN_BUCKET_LUA = """
local key = KEYS[1]
local now = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])         -- tokens per sec
local capacity = tonumber(ARGV[3])

local last = redis.call('HMGET', key, 'tokens', 'last')
local tokens = tonumber(last[1]) or capacity
local last_ts = tonumber(last[2]) or now

local delta = math.max(0, now - last_ts) * rate
tokens = math.min(capacity, tokens + delta)
local allowed = tokens >= 1
if allowed then tokens = tokens - 1 end

redis.call('HMSET', key, 'tokens', tokens, 'last', now)
redis.call('EXPIRE', key, math.ceil(capacity / rate * 2))

return allowed and 1 or 0
"""

IMPORTANT

분산 rate limit 는 Redis 한 곳 으로 원자적 카운터. 분산 락 없이 Lua 스크립트.

HTTP 응답 표준

HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1719318060

{"error": "rate_limit_exceeded", "retry_after": 30}

어디서?

flowchart LR
    Client --> Edge["CDN / WAF<br/>(IP 단위)"]
    Edge --> GW["API Gateway<br/>(API key 단위)"]
    GW --> App["App<br/>(user 단위)"]
    App --> DB["DB<br/>(쿼리 단위)"]
레이어단위
CDNIP, geographic
WAF의심 패턴
API GatewayAPI key, OAuth token
Appuser, tenant
DBquery, connection

Tier-based Rate Limit

limits = {
    "free":    {"per_min": 60, "burst": 100},
    "pro":     {"per_min": 600, "burst": 1000},
    "enterprise": {"per_min": 10000, "burst": 50000},
}

Stripe, OpenAI 같은 유료 API 의 표준.

흔한 함정

WARNING

  1. 단일 노드 in-memory counter = N 노드면 N배 한도. 분산 store 필수.
  2. IP 기반만 = NAT 뒤 수천 사용자가 한 IP. user + IP 조합.
  3. Retry-After 무시 (클라이언트) = 무한 retry → DDoS 자기 자신.
  4. Burst 너무 큼 = bucket 가득 차면 순간 throughput 폭증. 백엔드 보호 안 됨.

알고리즘 선택 기준

flowchart TD
    Q1["메모리 제약 있음?"] -->|"예"| Q2["Burst 허용?"]
    Q1 -->|"아니오"| SL["Sliding Log 적합"]
    Q2 -->|"예"| TB["Token Bucket"]
    Q2 -->|"아니오"| GCRA["GCRA or Leaky Bucket"]
    TB --> Q3["분산 환경?"]
    GCRA --> Q3
    Q3 -->|"예"| REDIS["Redis Lua 스크립트"]
    Q3 -->|"아니오"| MEM["In-memory"]

클라이언트 구현 패턴

클라이언트 측에서 429 를 올바르게 처리하지 않으면 오히려 서버에 더 큰 부하를 줄 수 있습니다.

import time
import random

def api_call_with_backoff(fn, max_retries=5):
    for attempt in range(max_retries):
        response = fn()
        if response.status_code != 429:
            return response

        retry_after = int(response.headers.get('Retry-After', 1))
        # 지수 백오프 + 지터
        wait = retry_after + (2 ** attempt) + random.uniform(0, 1)
        time.sleep(wait)

    raise Exception("Rate limit 초과: 재시도 실패")

IMPORTANT

Retry-After 헤더를 반드시 준수하세요. 무시하고 즉시 재시도하면 자기 DDoS 가 됩니다.

Rate Limit 모니터링

관찰해야 할 핵심 지표:

지표설명
rate_limit_hits_total총 차단 횟수, spike = 공격 또는 버그
tokens_remaining버킷 잔여 토큰, 낮으면 한도 근접
p99 latencyRedis 조회 추가 지연 모니터링
Top-N blocked users악의적 사용자 또는 버그 클라이언트 탐지
# Prometheus 메트릭 예시
from prometheus_client import Counter

rate_limit_hits = Counter(
    'rate_limit_hits_total',
    'Rate limit 에 걸린 요청 수',
    ['endpoint', 'user_tier']
)

@app.route('/api/resource')
def resource():
    if not rate_limiter.allow(request.user):
        rate_limit_hits.labels(
            endpoint='/api/resource',
            user_tier=request.user.tier
        ).inc()
        return jsonify(error='rate_limit_exceeded'), 429

실전 운영 고려사항

  • Warm-up 정책: 신규 사용자는 초반 더 관대하게 처리하여 정상 사용 패턴 파악.
  • Rate Limit 예외: 내부 서비스, health check endpoint 는 화이트리스트 처리.
  • 비동기 카운터: 극한 성능 필요 시 로컬 카운터로 먼저 판단, Redis 는 비동기 동기화 (근사).
  • 다중 버킷: 분당 + 초당 동시 적용.
limits = {
    'per_second': 10,
    'per_minute': 100,
    'per_hour': 2000,
}
# 셋 다 통과해야 허용 (AND 조건)

관련 위키

이 글의 용어 (6개)
[Concurrency] Backpressure: 흐름 제어로 시스템 보호concurrency
정의 Backpressure = 생산자가 소비자 속도에 맞춰 자기 속도 조절. 분산 시스템의 cascade failure 방지. [!IMPORTANT] Backpressure 의…
[Concurrency] Circuit Breaker: cascade failure 방어concurrency
정의 Circuit Breaker = 전기 회로의 차단기처럼, 백엔드 다운 / 느림 시 호출 자체를 차단 → 빠른 실패 + 백엔드 회복 시간. 와 함께 cascade failur…
[Concurrency] Retry + Exponential Backoff + Jitterconcurrency
정의 Retry with Exponential Backoff + Jitter = 실패 시 점점 긴 간격으로 재시도, 랜덤 분산. 분산 시스템의 thundering herd / r…
[Pattern] API Gateway: BFF, 라우팅, auth, rate limitdistributed-systems
정의 API Gateway = 클라이언트와 마이크로서비스 사이의 단일 진입점. 라우팅, auth, rate limit, transformation, monitoring 의 cro…
[Redis] Sorted Set: Skiplist + Dict, 리더보드, 우선순위 큐database-internals
정의 Redis Sorted Set (ZSet) 은 멤버 + 점수 (score) 의 순서 보존 unique 집합. score 기준 정렬 + 멤버 기준 O(1) 조회 가 동시에 가…
[Redis] String / Bitmap / Bitfield: 가장 기본 + 가장 강력database-internals
정의 Redis String 은 binary-safe 한 바이트 열. 최대 512 MB. 문자열만이 아니라 정수, 부동소수, 직렬화된 객체, 비트맵 (Bitmap), 패킹된 정수…

💬 댓글

사이트 검색 / 명령어

검색

스크롤 = 확대/축소 · 드래그 = 이동 · 0 = 원래 크기 · ESC = 닫기