Files
2nd/10_Wiki/Topics/Coding/Backend_Rate_Limiting.md
T
2026-05-09 21:08:02 +09:00

4.1 KiB

id, title, category, status, source_trust_level, verification_status, created_at, updated_at, tags, tech_stack, applied_in, aliases
id title category status source_trust_level verification_status created_at updated_at tags tech_stack applied_in aliases
backend-rate-limiting Rate Limiting — Token Bucket vs Sliding Window Coding draft B conceptual 2026-05-09 2026-05-09
backend
rate-limit
throttling
vibe-coding
language applicable_to
TypeScript / Redis
Backend
API gateway
throttle
leaky bucket
sliding window
fixed window

Rate Limiting

입구 차단으로 시스템 보호. per-user / per-IP / per-API key 다중 차원. 알고리즘 4종 — Fixed window / Sliding window / Token bucket / Leaky bucket. 분산 환경은 Redis 가 기본.

📖 핵심 개념

  • Fixed window: "1분당 100회". 단순. 경계에서 burst 두 배.
  • Sliding window: 시간 가중. 경계 burst 없음. 계산 복잡.
  • Token bucket: 토큰 N개, 초당 R 회복. burst 허용. 현실적 선택.
  • Leaky bucket: 큐 + 일정 속도 처리. 평탄화.

💻 코드 패턴

Token bucket — Redis Lua atomic

-- KEYS[1] = bucket key, ARGV[1] = capacity, ARGV[2] = refillPerSec, ARGV[3] = now
local data = redis.call('HMGET', KEYS[1], 'tokens', 'ts')
local tokens = tonumber(data[1]) or tonumber(ARGV[1])
local ts = tonumber(data[2]) or tonumber(ARGV[3])
local elapsed = math.max(0, tonumber(ARGV[3]) - ts)
tokens = math.min(tonumber(ARGV[1]), tokens + elapsed * tonumber(ARGV[2]))
local allowed = 0
if tokens >= 1 then tokens = tokens - 1; allowed = 1 end
redis.call('HMSET', KEYS[1], 'tokens', tokens, 'ts', ARGV[3])
redis.call('EXPIRE', KEYS[1], 3600)
return allowed
// Express middleware
async function rateLimit(req, res, next) {
  const key = `rl:${req.ip}:${req.user?.id ?? 'anon'}`;
  const allowed = await redis.eval(LUA, 1, key, /*cap*/100, /*refill*/10, Date.now()/1000);
  if (!allowed) {
    res.setHeader('Retry-After', '1');
    return res.status(429).json({ error: 'rate_limited' });
  }
  next();
}

Sliding window — Redis sorted set

async function slidingAllow(key: string, limit: number, windowSec: number): Promise<boolean> {
  const now = Date.now();
  const windowStart = now - windowSec * 1000;
  const tx = redis.multi();
  tx.zremrangebyscore(key, 0, windowStart);
  tx.zadd(key, now, `${now}-${Math.random()}`);
  tx.zcard(key);
  tx.expire(key, windowSec);
  const [, , count] = await tx.exec();
  return (count as number) <= limit;
}

Layered limits

const limits = [
  { key: `rl:ip:${req.ip}`, limit: 100, windowSec: 60 },          // IP 기준
  { key: `rl:user:${userId}`, limit: 1000, windowSec: 3600 },     // 사용자 시간당
  { key: `rl:plan:${userId}`, limit: 50000, windowSec: 86400 },   // 일일
];
for (const l of limits) {
  if (!await slidingAllow(l.key, l.limit, l.windowSec)) return res.status(429).end();
}

🤔 의사결정 기준

차원
Per-IP rl:ip:${req.ip}
Per-user rl:user:${userId}
Per-API key rl:apikey:${key}
Per-endpoint rl:endpoint:${path}:${userId}
비용 큰 작업 (PDF 생성) per-user 더 엄격
알고리즘 권장
단순 / 정확성 보통 Fixed window
정확한 sliding 필요 Sliding window log (sorted set)
burst 허용 + 평균 통제 Token bucket
평탄한 처리 (DB 보호) Leaky bucket

안티패턴

  • 메모리 in-process counter: 다중 인스턴스에서 의미 없음. Redis 또는 외부.
  • X-Forwarded-For 검증 없이 IP 사용: 클라이언트가 위조 가능. 신뢰 가능한 proxy 만.
  • 429 응답에 Retry-After 누락: 클라이언트가 언제 재시도할지 모름.
  • 인증 endpoint 에 너무 헐거운 limit: brute force 무방비.
  • per-IP 만: NAT 뒤 다수 사용자 = 한 IP. user 기준 병행.
  • rate limit 자체에 타임아웃 누락: Redis 응답 늦으면 모든 요청 block.
  • fail-open vs fail-closed 결정 안 함: Redis 다운 시 다 통과 (fail-open) 또는 다 차단 (fail-closed). 명시.

🤖 LLM 활용 힌트

  • 다층 limit (IP + user + plan) + Redis sorted set 권장.
  • 429 + Retry-After + JSON error code 표준.

🔗 관련 문서