LLM Gateway Admission Control (3) — Spend Limits 2026-09-08

Team과 API Key의 일·월 지출 한도를 확인하고, 추정 사용료의 즉시 반영과 Worker의 정확한 정산으로 사용료 Cache를 관리합니다.

LLM Gateway Admission Control

  1. Rate Limits
  2. Budget Control
  3. Spend Limits — current

TL;DR

Preflight가 한도와 기간별 사용료 Cache를 비교하고, Postflight는 추정 사용료를 누적하며 Worker는 정확한 정산 결과로 Cache를 갱신하는 흐름

정책

  • Team 월, API Key 일·월 한도 중 설정된 항목만 확인하고 사용료를 집계합니다.
  • 집계 기간은 Gateway가 request를 받은 호출 시작 시각을 기준으로 UTC 일·월을 구분합니다. 응답이나 정산이 늦어져도 귀속 기간은 바뀌지 않습니다.
  • 설정된 한도 중 하나라도 소진되면 429로 거절합니다. 정책이 없는 scope·기간은 제한하지 않습니다.

요약

Spend Limits는 현재 잔액과 별개로 기간별 지출을 제한합니다.
Postflight에서 추정 사용료를 Cache에 누적해 정산 gap을 줄이고, Worker가 정확한 정산값으로 갱신합니다.

1. Redis key를 정책과 사용료로 나눕니다

quota:{teamId}:spend:policy                                 # Team·API Key 한도 정책
quota:{teamId}:spend:usage:team:month:202609                # Team 월 사용료
quota:{teamId}:spend:usage:api_key:<apiKeyId>:day:20260908  # API Key 일 사용료
quota:{teamId}:spend:usage:api_key:<apiKeyId>:month:202609  # API Key 월 사용료

policy는 한도, usage는 누적 사용료이며 정책이 있는 scope·기간만 집계하고 캐시합니다.
{teamId}는 같은 Team의 key를 한 Cluster slot에 모으는 hash tag이고, <apiKeyId>는 중괄호 없이 넣는 식별자입니다. 덕분에 정책과 사용료를 하나의 Lua에서 처리할 수 있습니다.

scope기간한도 예시
Teammonthly$200
API Keydaily$10
API Keymonthly$100
  • 정책은 대상·기간별 DB row로 관리하며, limit_usd는 NOT NULL인 0 이상의 금액입니다. row가 없으면 제한하지 않고, 0이면 차단합니다.
  • 설정된 한도는 모두 통과해야 합니다. 위 예시에서 API Key의 일 사용료가 $5여도 Team 월 사용료가 $200이면 거절합니다.
  • Cache 금액은 정수 scaled USD로 저장하고, 한도도 같은 단위로 변환합니다.

2. Inference path에서 비교하고 누적합니다

Preflight는 설정된 한도를 함께 확인합니다

API Key 인증으로 Team과 Key를 찾은 뒤, 하나의 Lua에서 설정된 한도와 호출 시작 시각이 속한 기간의 사용료를 비교합니다. 적용할 정책이 없으면 Spend Limits 사용료 조회를 생략합니다.
누적 사용료가 설정된 한도 이상이면 Provider를 호출하지 않습니다.

호출 시작 시각은 Inference Record에도 남겨 Postflight와 Worker가 같은 기간에 반영하도록 합니다. 같은 inference의 retry·fallback에서도 이 시각을 유지합니다.

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

{
  "error": {
    "type": "insufficient_quota",
    "code": "spend_limit_exceeded",
    "message": "API Key daily spend limit exceeded."
  }
}

Postflight는 기간별 사용료를 즉시 누적합니다

성공한 inference의 추정 사용료가 $0.02라면 Account Balance Cache에서 $0.02를 뺍니다. API Key 일 한도만 설정돼 있다면 해당 일 사용료 Cache에만 $0.02를 더하고, 다음 request에서 한도와 비교합니다.

잔액 차감과 정책이 있는 사용료 Cache 증가는 같은 Lua에서 한 번에 처리하며, Cache hit 시 추가 Redis 왕복은 없습니다.
Lua는 실행 오류가 나도 앞선 변경을 rollback하지 않습니다. 일부 Cache만 갱신되는 일을 줄이기 위해, 필요한 key와 값이 정상인지 먼저 확인한 뒤 갱신합니다.
Record 재전송이나 Worker 재시도는 이 누적을 다시 실행하지 않습니다. Redis 반영 여부가 불명확해도 다시 더하지 않고, 다음 정산값으로 갱신합니다.

3. Worker가 정산값으로 Cache를 갱신합니다

Worker는 scheduler 주기마다 미정산 Record를 모아 처리합니다. Usage Charge 생성은 한도 정책과 독립적이며, Spend Limits 집계만 정책 유무에 따라 생략합니다.

  1. Record의 usage와 가격표로 Usage Charge를 계산합니다.
  2. 정책이 있는 Team 월·API Key 일·월에 사용료를 누적하고, Usage Charge·집계값·처리 완료 상태를 같은 DB transaction으로 확정합니다.
  3. DB commit 뒤 확정된 누적 사용료로 Cache를 갱신합니다. 추정값에 Usage Charge를 다시 더하지 않습니다.

같은 Charge를 같은 scope·기간에 두 번 집계하지 않으며, 같은 Team의 Worker는 DB 반영부터 Cache 갱신까지 순서대로 처리합니다.
Cache 갱신에 실패해도 DB 정산은 되돌리지 않습니다. 다음 Worker 실행에서 현재 기간의 집계값으로 Cache를 다시 갱신하며, 새 Usage Charge가 없어도 수행합니다.

호출과 정산의 날짜가 달라도 같은 기간에 반영합니다

9월 30일 23:59 UTC에 시작한 호출이 10월 1일에 끝나고 정산돼도, 사용료는 9월 30일·9월에 반영합니다.
종료된 기간은 DB 집계만 갱신하고, 만료된 Redis key를 다시 만들거나 현재 기간으로 옮겨 누적하지 않습니다.

일 한도는 자정에, 월 한도는 매월 1일에 새로 시작합니다. 일 한도가 $10이면 자정 직전과 직후에 각각 $10을 사용할 수 있습니다. 이는 최근 24시간 합계를 제한하는 정책과 다릅니다.

4. Cache 수명과 운영 경계를 관리합니다

대상수명·갱신
정책 CacheTTL 5분 + 0~1분 jitter. 조회 시 연장하지 않고, 변경은 만료 후 다시 읽을 때 반영
사용료 CacheUTC 기간 종료 뒤 지연 처리 여유를 두고 만료. Postflight가 기간 경계를 연장하지 않음

Cache miss는 DB에서 복구합니다

정책이 있는 현재 기간의 사용료를 DB에서 읽어 빈 key만 채우고, 이미 채워진 Cache는 덮지 않습니다.
DB 조회는 성공했지만 아직 집계값이 없다면 Cache를 0원으로 시작합니다. 조회 자체가 실패한 경우에만 503을 반환합니다.
애플리케이션 내 동일 key 조회는 하나로 합치고, connection pool로 동시 조회를 제한해 DB를 보호합니다. 정책 없음도 정상 조회 결과로 캐시합니다.

정책 변경은 사용 기록을 바꾸지 않습니다

한도 추가·재활성화 시 집계값이 없으면 0원으로 시작하고, Worker가 기존 Usage Charge를 집계할 때까지의 반영 지연은 허용합니다. 초기 집계와 후속 정산은 중복되지 않게 처리합니다. 한도 금액만 바꾸면 집계값을 유지하고, 제거하면 제한용 집계·Cache 누적을 중단합니다.

지금은 TTL만 사용합니다. 향후 CP의 Cache eviction을 추가하면, eviction 실패 시 허용할 반영 지연에 맞춰 TTL을 늘릴 수 있습니다.

추정값의 오차와 장애를 구분합니다

상황처리·영향
실행 중인 inference아직 사용료가 반영되지 않아 추가 request가 통과할 수 있음
추정값과 실제 사용료의 차이제한이 늦거나 일찍 걸릴 수 있으며, 정산값으로 보정
정산값으로 Cache 갱신정산 기준 이후의 미정산 추정분은 다음 정산까지 Cache에서 빠질 수 있음
Preflight Redis timeout·Lua 오류503, fail-open하지 않음
Redis 데이터 유실DB look-aside와 Worker의 주기적 갱신으로 복구. 미정산 추정분은 다음 정산까지 누락될 수 있음

예상 사용료를 미리 예약하지 않고 여러 inference를 동시에 허용하는 대신, Postflight의 추정 사용료 누적으로 한도 초과를 최소화합니다.
운영 지표로는 Lua p99, 추정 오차, Cache 갱신 실패, scope별 초과 사용료, Team 월 집계 row의 경합을 확인합니다.

참고