---
id: cs-rate-limit-algorithms
title: Rate Limit 알고리즘 — Token / Leaky / Sliding
category: Coding
status: draft
source_trust_level: B
verification_status: conceptual
created_at: 2026-05-09
updated_at: 2026-05-09
tags: [cs, rate-limit, algorithm, vibe-coding]
tech_stack: { language: "TS / Redis", applicable_to: ["Backend"] }
applied_in: []
aliases: [token bucket, leaky bucket, sliding window, fixed window, GCRA]
---

# Rate Limit 알고리즘

> 4가지: **Fixed window / Sliding window / Token bucket / Leaky bucket**. 정확도 / 메모리 / burst 허용 trade-off. **Redis 분산 = sliding window 또는 GCRA**.

## 📖 핵심 개념
- Burst: 짧은 spike 허용?
- Fairness: 사용자 간 공평?
- 정확성: 경계 시점 정확?
- 메모리: 사용자당 얼마나?

## 💻 코드 패턴

### Fixed window (가장 단순)
```ts
async function fixedWindow(key: string, limit: number, windowSec: number): Promise<boolean> {
  const bucket = `rl:${key}:${Math.floor(Date.now() / 1000 / windowSec)}`;
  const count = await redis.incr(bucket);
  if (count === 1) await redis.expire(bucket, windowSec);
  return count <= limit;
}
```

⚠️ 경계에서 burst 가능: 1분에 100 limit + 0:59 에 100, 1:00 에 또 100 = 1초에 200.

### Sliding window log
```ts
async function slidingWindowLog(key: string, limit: number, windowMs: number): Promise<boolean> {
  const now = Date.now();
  const bucket = `rl:${key}`;
  
  await redis.zremrangebyscore(bucket, 0, now - windowMs);
  const count = await redis.zcard(bucket);
  if (count >= limit) return false;
  
  await redis.zadd(bucket, now, now);
  await redis.pexpire(bucket, windowMs);
  return true;
}
```

→ 정확. 단 메모리 = limit (각 요청 timestamp).

### Sliding window counter (Cloudflare 방식)
```ts
async function slidingWindowCounter(key: string, limit: number, windowSec: number): Promise<boolean> {
  const now = Date.now() / 1000;
  const cur = Math.floor(now / windowSec);
  const prev = cur - 1;
  const elapsed = (now / windowSec) - cur; // 0..1

  const [curCount, prevCount] = await Promise.all([
    redis.get(`rl:${key}:${cur}`).then(Number),
    redis.get(`rl:${key}:${prev}`).then(Number),
  ]);

  // 가중 합
  const estimate = prevCount * (1 - elapsed) + curCount;
  if (estimate >= limit) return false;

  await redis.incr(`rl:${key}:${cur}`);
  await redis.expire(`rl:${key}:${cur}`, windowSec * 2);
  return true;
}
```

→ Fixed 의 burst 문제 + 정확성 + 작은 메모리.

### Token bucket
```ts
class TokenBucket {
  private tokens: number;
  private lastRefill: number;
  
  constructor(private capacity: number, private refillPerSec: number) {
    this.tokens = capacity;
    this.lastRefill = Date.now();
  }
  
  consume(n = 1): boolean {
    this.refill();
    if (this.tokens < n) return false;
    this.tokens -= n;
    return true;
  }
  
  private refill() {
    const now = Date.now();
    const elapsed = (now - this.lastRefill) / 1000;
    this.tokens = Math.min(this.capacity, this.tokens + elapsed * this.refillPerSec);
    this.lastRefill = now;
  }
}
```

→ Burst (capacity 만큼) 허용. 표준 패턴.

### Token bucket Redis (atomic Lua)
```lua
-- token-bucket.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local refill_rate = tonumber(ARGV[2])  -- per second
local now = tonumber(ARGV[3])
local cost = tonumber(ARGV[4])

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

local elapsed = (now - last) / 1000
tokens = math.min(capacity, tokens + elapsed * refill_rate)

if tokens < cost then
  redis.call('HMSET', key, 'tokens', tokens, 'last', now)
  redis.call('PEXPIRE', key, math.ceil(capacity / refill_rate * 1000))
  return 0
end

tokens = tokens - cost
redis.call('HMSET', key, 'tokens', tokens, 'last', now)
redis.call('PEXPIRE', key, math.ceil(capacity / refill_rate * 1000))
return 1
```

### Leaky bucket
```ts
// 요청 = drop. 일정 rate 로 흘러나감.
class LeakyBucket {
  private queue: number[] = [];
  
  constructor(private capacity: number, private leakPerSec: number) {}
  
  add(now: number): boolean {
    this.leak(now);
    if (this.queue.length >= this.capacity) return false;
    this.queue.push(now);
    return true;
  }
  
  private leak(now: number) {
    while (this.queue.length > 0) {
      const oldest = this.queue[0];
      const elapsed = (now - oldest) / 1000;
      if (elapsed >= 1 / this.leakPerSec) {
        this.queue.shift();
      } else break;
    }
  }
}
```

→ 출력 rate 일정. 미들 / network shaping 에 강.

### GCRA (Generic Cell Rate Algorithm)
```ts
// 메모리 = 2 숫자 / key. 정확.
// 1번 호출 = O(1).
async function gcra(key: string, periodMs: number, burst: number): Promise<boolean> {
  const now = Date.now();
  const arrival = await redis.get(`gcra:${key}`).then(Number);
  const tat = Math.max(arrival || 0, now);
  
  const newTat = tat + periodMs;
  const allowAt = newTat - burst * periodMs;
  
  if (now < allowAt) return false;
  await redis.set(`gcra:${key}`, newTat, 'PX', burst * periodMs);
  return true;
}
```

→ Stripe 가 사용. 메모리 효율 + 정확.

### Distributed (다중 서버)
```ts
// Redis SETEX + atomic INCR
// 또는 위 Lua script
// 또는 가까운 dedicated rate-limit service (Envoy + ratelimit)
```

### 멀티 키 (per IP + per user)
```ts
const ipOk = await rateLimit(`ip:${ip}`, 1000, 60);
const userOk = await rateLimit(`user:${userId}`, 100, 60);
if (!ipOk || !userOk) return 429;
```

### Cost-weighted (비싼 endpoint)
```ts
// Token bucket: 일반 = 1 token, 큰 = 10 token
const cost = endpoint === '/expensive' ? 10 : 1;
const ok = await tokenBucket.consume(cost);
```

## 🤔 의사결정 기준
| 상황 | 추천 |
|---|---|
| 단순 / 부정확 OK | Fixed window |
| 정확 / 작은 메모리 | Sliding window counter / GCRA |
| Burst 허용 | Token bucket |
| 일정 출력 (queue 처럼) | Leaky bucket |
| 분산 / 큰 규모 | Redis Lua + Sliding/GCRA |
| 정확 + 효율 | GCRA |

## ❌ 안티패턴
- **Memory 만 (per-server)**: 분산 환경 비공정.
- **Fixed window prod**: 경계 burst 위험.
- **Limit 없음 prod**: DoS 취약.
- **사용자 한 형태만**: IP / user / API key 각각.
- **429 + 큰 cost (DB query 까지 도달)**: gateway 에서 cut.
- **Retry-After 헤더 없음**: 클라가 무한 재시도.
- **Test 환경 같은 limit**: prod 만 강.

## 🤖 LLM 활용 힌트
- 분산 + 정확 = GCRA / Sliding window counter.
- Burst = Token bucket.
- 다중 key (IP + user + API key).
- 429 + Retry-After.

## 🔗 관련 문서
- [[Backend_Rate_Limiting]]
- [[DB_Redis_Patterns]]
- [[Backend_API_Gateway_BFF]]
