속도 제한¶
Continuum Router는 남용을 방지하고, 리소스를 공정하게 배분하며, 백엔드 서비스를 과부하로부터 보호하기 위해 요청 속도를 제한합니다. 속도 제한 시스템은 토큰 버킷 알고리즘을 기반으로 다중 계층 보호를 수행합니다.
개요¶
라우터는 다중 계층 속도 제한을 구현합니다:
- 클라이언트별 제한: 개별 클라이언트가 시스템을 과부하시키는 것을 방지
- 백엔드별 제한: 백엔드를 설정된 용량 이내로 유지하되 우회 라우팅을 우선 적용하며, 해당 모델을 서빙하는 모든 백엔드가 한도에 도달했을 때만 요청을 거절합니다 (기본값 꺼짐, 백엔드별 제한 참고)
- API 키별 제한: 인증된 키 단위로 제한을 적용하고, 키별 오버라이드를 지원
- 사용자별·팀별 제한: 호출자의 사용자 또는 팀 식별자를 기준으로 제한하므로, 한 사용자의 여러 키 또는 한 팀의 모든 구성원이 하나의 허용량을 공유합니다
- 모델별 제한: 특정 (고비용) 모델 보호
- 전역 제한: 전체 시스템 안정성 보장
- 토큰 예산: 요청 수가 아니라 분당 토큰을 제한하며 요청 시작과 완료 시점에 각각 적용됩니다 (기본값 꺼짐, 토큰 예산 참고)
제한이 적용되는 위치¶
각 제한 차원은 기준이 되는 식별 정보가 준비되는 가장 바깥 계층에 장착되며, 차원마다 단일 지점에서만 집행됩니다:
- 전역 (
limits.global): 인증 및 CORS보다 바깥에 위치한 애플리케이션 전체 단일 계층에서 집행됩니다. 모든 요청이 정확히 한 번 차감되며, 여기에는 이후 인증 단계에서401로 거절되는 API 요청(API 키 무차별 대입 시도 계량), 인증에 실패한 요청을 포함한 관리자 요청,/status,/version, WebUI, Files, 메트릭, CORS 프리플라이트,404폴백이 모두 포함됩니다. - 클라이언트별 (
limits.per_client): API 라우트(/v1/*,/anthropic/*,/embed_sparse)의 인증 바깥 계층에 장착됩니다. 인증이 실행되기 전에 연결의 피어 주소를 키로 사용하므로, 단일 출발지 IP에서 발생하는 미인증 요청 폭주가 공유 전역 버킷을 소진하기 전에 먼저 격리합니다. - 식별 차원 (
per_api_key,per_user,per_team,per_model):AuthContext가 생성된 뒤인 API 라우트의 인증 안쪽 계층에 장착됩니다. 이 계층은 전역 버킷이나 클라이언트별 버킷을 차감하지 않습니다. - 백엔드별 (
limits.per_backend): 요청 유입 시점에는 아직 대상 백엔드가 정해지지 않으므로, 진입 계층이 아니라 백엔드 선택 단계에서 적용됩니다. 선택기는 버킷이 빈 백엔드를 후보에서 제외해 같은 모델을 서빙하는 다른 백엔드로 라우팅하며, 실제로 요청이 디스패치될 때 선택된 백엔드 버킷에서 토큰을 한 번 차감합니다. 모든 후보 백엔드가 소진되었을 때만 요청을 거절합니다. 자세한 내용은 백엔드별 제한을 참고하십시오.
따라서 API 라우트의 요청 평가 순서는 전역 -> 클라이언트별 -> 인증 -> 키별 -> 사용자별 -> 팀별 -> 모델별 -> 핸들러 -> 선택(백엔드별) -> 디스패치입니다. 각 계층이 서로 겹치지 않는 차원을 집행하고 백엔드별 차감은 디스패치당 한 번만 수행되므로, 어떤 경로를 거치더라도 동일한 버킷에서 중복 차감되지 않습니다.
예외 경로 및 미인증 트래픽 처리:
- 헬스 프로브: 전역 계층은
/health,/health/ready,/healthz를 무조건 면제합니다. 이 핸들러들은 제한된 인메모리 상태만 읽으므로, 라우터가 포화된 상태에서도 오케스트레이터의 생존·준비성 프로브에 정상 응답합니다. - 메트릭 엔드포인트:
metrics.enabled가true일 때 설정된metrics.path에 한해 면제됩니다 (metrics.enabled가 꺼져 있으면/metrics도 일반 미매칭 경로와 동일하게 계량됩니다). 단, 활성화된 메트릭 엔드포인트를 스크레이프할 때마다 백엔드 게이지를 재구성하고 전체 레지스트리를 직렬화하므로, 메트릭 카디널리티에 비례해 비용이 증가하며 속도 제한기로는 스크레이프 폭주를 막을 수 없습니다. 반드시metrics.auth와 네트워크 정책으로 메트릭 엔드포인트를 보호하십시오. - 미인증 및 미매칭 트래픽:
401로 거절되는 요청이나404폴백에 도달하는 잘못된 경로 요청도 API 트래픽과 동일한limits.global버킷을 소비합니다. API 경로에서는per_client가 단일 IP의 폭주를 해당 주소의 버킷에서 먼저 차단하지만, 비 API 경로 트래픽은limits.global만으로 제한됩니다. 예상 API 호출량보다 여유 있게limits.global을 설정하거나, 앞단 프록시에서 불필요한 트래픽을 먼저 차단하십시오.
헤더 귀속과 거절 우선순위¶
하나의 요청을 여러 제한 계층이 순서대로 평가하므로, 클라이언트에 반환되는 헤더는 다음 규칙을 따릅니다:
- 허용된 요청 (
2xx):x-ratelimit-*헤더는 인증된 식별 정보에 귀속된 할당량(per_api_key)만 보고합니다. 키별 차원이 평가되지 않은 요청에는x-ratelimit-*헤더가 포함되지 않습니다. - 성공 응답에서 공유 버킷 비노출:
limits.global,per_model,per_backend,per_user,per_team은 둘 이상의 호출자가 공유하는 용량이며,per_client는 동일 출발지 IP(NAT, 이그레스 게이트웨이, 사내 프록시 등) 뒤의 모든 호출자가 공유하고 인증 이전에 평가됩니다. 이 차원들은 모두 설정대로 집행되지만, 허용된 응답에는 잔량을 노출하지 않습니다. 클라이언트에 할당량 헤더를 제공하려면per_api_key를 설정하십시오. - 거절된 요청 (
429): 요청을 차단한 차원의 값이error.details.limit_type,retry-after,x-ratelimit-*에 그대로 기록됩니다 (공유 버킷에 의한 거절이라도 클라이언트가 재시도 시점을 판단할 수 있도록 값을 제공합니다). 공유 차원이 토큰 예산을 거부한 경우만 예외이며, 그 버킷의 값은 숨깁니다. 자세한 내용은 토큰 예산을 참고하십시오. 전역 차원과 안쪽 차원이 동시에 한도를 초과한 경우에는 요청 경로의 가장 바깥에 있는 전역 거절이 반환됩니다.Retry-After는 모든 경로에서 최소1이상입니다. - CORS 가시성: CORS가 활성화되면
retry-after와x-ratelimit-*헤더가 항상Access-Control-Expose-Headers에 추가되어, 브라우저 스크립트가 상태 코드뿐 아니라 재시도 힌트도 읽을 수 있습니다. 또한 프리플라이트 계량을 위해 CORS 계층 바깥에 위치한 전역 계층도 즉시 반환하는429응답에Access-Control-Allow-Origin헤더를 직접 포함합니다. - 바깥 계층의 토큰 차감: 안쪽 계층(예:
per_api_key)에서 거절된 요청이라도, 안쪽 계층에 도달하기 전에 이미 통과한 전역 버킷과 해당 주소의 클라이언트별 버킷에서는 각각 토큰이 1개씩 차감됩니다.
헤더가 드러내는 것과 드러내지 않는 것¶
성공 응답의 x-ratelimit-* 헤더는 호출자가 인증을 통해 증명한 본인의 per_api_key 버킷만 나타내므로, 호출자가 이미 보유한 권한 밖의 정보를 노출하지 않습니다. per_api_key 평가를 거치지 않은 모든 응답에는 할당량 헤더가 전혀 포함되지 않습니다:
401인증 실패 및404폴백 응답/version과 같은 미인증200엔드포인트- IP
whitelist또는bypass_keys에 해당하는 요청 - API 경로에 대한 미인증 호출자의 요청
이 일관성은 부채널을 통한 키 탐색을 방지합니다. 클라이언트별(per_client) 계층은 인증보다 먼저 실행되므로, 만약 401 응답에 per_client 헤더를 포함한다면 bypass_keys 보유자(면제되어 헤더 없음)와 일반 무효 토큰(계량되어 헤더 있음)이 구분되어 공격자가 요청 한 번으로 우회 키 여부를 판별할 수 있습니다. 이를 막기 위해 per_client 할당량은 응답 헤더에 보고하지 않습니다.
다만 permissive 인증 모드에서 per_api_key를 설정한 경우, 등록된 키에는 키별 헤더가 포함되고 미등록 토큰에는 헤더가 포함되지 않아 헤더 유무로 두 경우가 구분됩니다. permissive 모드는 보안 경계가 아니라 개발 및 신뢰 네트워크용 편의 모드로, 유효한 키와 무효한 키에 이미 동일한 모델과 백엔드 접근 권한을 부여하므로 이 차이는 보안 경계를 침해하지 않습니다. 키 유효성 노출을 차단해야 하는 배포 환경에서는 api_keys.mode: blocking을 설정하여, 미등록 키가 식별 차원에 도달하기 전에 401로 거절되도록 구성하십시오.
할당량을 소비하지 않고 읽기¶
GET /v1/quota는 신원에 묶인 버킷을 차감하지 않고 호출자 본인의 한도와 남은 값을 보고하므로, 클라이언트가 읽은 값을 소비하지 않고 반복 조회할 수 있습니다. per_api_key 버킷이나 max_tokens_budget에 이미 도달한 키도 remaining: 0과 함께 200을 받습니다.
문서에는 인증된 신원에 묶인 값만 담깁니다:
request_rate,token_rate: 키별 요청 버킷과 분당 토큰 버킷.user_request_rate,team_request_rate: 설정된 경우 사용자별·팀별 요청 버킷. 해당 사용자나 팀의 키가 함께 사용하며 다른 신원은 포함하지 않습니다.inflight:max_in_flight_requests에 대한 현재 동시 처리 수.token_budget: 기록된 사용량과 처리 중 예약을 뺀 장기max_tokens_budget.hub_*: 토큰이 동기화된 키와 일치할 때의 허브 티어 윈도.
공유 함대 수준(global, per_client, per_model, per_backend), 배치 차원, 키 ID·이름·사용자·팀은 포함하지 않습니다. 할당량 응답에는 x-ratelimit-* 또는 x-continuum-ratelimit-* 헤더가 없습니다.
공유 global과 per_client 상한은 /v1/models 비용인 0.1로 계속 차감되며, 둘 중 하나가 일반 429로 조회를 거부할 수 있습니다. 이 경로는 다른 API 경로처럼 인증 전에 계량되지만, 두 상한의 수준은 응답에 실리지 않습니다.
미인증 호출자: permissive 모드에서 토큰이 없거나 미등록 토큰인 요청은 dimensions: []와 함께 200을 받으며, 한도가 설정되지 않은 등록 키와 바이트 단위로 같습니다. blocking 모드에서는 기존 401과 403이 적용됩니다.
장기 예산도 보고하므로 permissive 모드의 구분은 per_api_key뿐 아니라 max_tokens_budget에도 적용됩니다. 둘 중 하나라도 설정된 키는 미등록 토큰이 만들 수 없는 문서를 받습니다. 키 유효성 노출을 차단해야 하는 배포 환경에서는 api_keys.mode: blocking을 설정하십시오.
스토어 장애: rate_limiting.redis.fallback_to_memory: true에서 Redis에 연결할 수 없으면 이 레플리카의 버킷을 읽고, 해당 행에 scope: "replica"를 표시하며 degraded: true와 함께 200을 반환합니다. fallback_to_memory: false이면 기존 503과 Retry-After: 5를 받습니다. 허브 공유 분당 스토어에 연결할 수 없으면 허브 행은 이 레플리카의 로컬 윈도에서 오며 degraded: true가 됩니다. 이는 해당 한도의 fail-open 규칙과 같습니다.
설정¶
기본 설정¶
rate_limiting:
enabled: true
storage: memory # 분산 설정의 경우 "redis"
limits:
per_client:
requests_per_second: 10
burst_capacity: 20
per_backend: # 옵트인: 진입 거절이 아닌 라우팅 필터, 백엔드별 제한 참고
requests_per_second: 100
burst_capacity: 200
global:
requests_per_second: 1000
burst_capacity: 2000
API 키별 속도 제한¶
API 키에는 다음 우선순위로 세 가지 메커니즘이 적용됩니다.
-
rate_limiting.limits안의per_api_key차원: 개별 오버라이드가 없는 모든 등록된 키에 적용되는 기본값입니다. -
api_keys섹션의 개별 키에 지정하는rate_limit오버라이드 (분당 요청 수): 설정하면 해당 키에서는per_api_key기본값을 대체합니다.rate_limit: 0으로 설정하면 해당 키의 키별 제한을 완전히 비활성화합니다. -
개별 키의 적용 속도를 세부 조정하는 두 가지 선택 값입니다.
rate_limit_burst는 해당 키에 적용되는 요청 속도의 버킷 용량입니다. 키 자체의rate_limit이 있으면 그 속도를, 없으면per_api_key속도를 따릅니다. 설정하지 않으면 초당 속도의 두 배로 계산한 용량을 유지합니다.rate_limit_tokens_per_minute는 해당 키의 분당 토큰 예산으로,per_api_key의 분당 예산을 대체합니다.0이면 이 키의 분당 예산을 비활성화하고, 설정하지 않으면 차원에 설정된 예산을 유지합니다. 나머지 수락 시점 차원(global,per_client,per_model)은 계속 이 키의 요청에 적용되며, 키별 일일 예산은 이 오버라이드에 포함되지 않습니다.
키별 제한은 등록된(인식된) 키에만 적용됩니다. 이 차원은 인증 계층이 부여한 식별 정보를 읽으므로, 익명 호출자(키가 없거나 등록되지 않은 키)는 인증 모드와 무관하게 키별 제한 대상이 되지 않습니다. 대신 인증 바깥 계층에 장착된 per_client와 global 제한은 이후 401 거절 여부와 상관없이 그대로 적용됩니다.
요청 수 per_api_key 버킷은 자격 증명을 도메인 분리한 배포-안정 해시로 키를 만듭니다. 따라서 storage: redis에서는 모든 복제본이 하나의 공유 버킷을 차감합니다. 이 해시에는 프로세스별 솔트가 없으며, 해시 값은 버킷 키로만 쓰이고 클라이언트에 전송되지 않습니다. 이 방식으로 업그레이드하면 기존 요청 수 per_api_key 버킷의 키가 한 번 바뀌고, 이전 Redis 키는 설정된 TTL로 만료됩니다 (이슈 #2899).
오버라이드 범위¶
키별 표면은 한 키의 요청 속도, 버스트, 분당 토큰 예산을 다룹니다. 다음 항목은 여기에 포함되지 않습니다.
- 한 사용자의 모든 키가 공유하는 예산: 이슈 #2381에서 따로 다룹니다.
- 사용자별·팀별 차원, 처리 중 요청 상한, 공정 큐잉(등급 오버라이드는
rate_limiting.tier_overrides에서 따로 설정합니다). - 키별 일일 토큰 예산:
rate_limit_tokens_per_minute는 분 버킷만 바꿉니다.
사용자별·팀별 속도 제한¶
per_user와 per_team은 자격 증명이 아니라 API 키가 이미 해석한 식별자를 기준으로 버킷을 만듭니다. 따라서 한 사용자의 두 키는 하나의 버킷을 함께 사용하고, 한 팀의 모든 구성원은 팀 버킷을 함께 사용합니다.
- 식별자 출처:
per_user는 키의 사용자 ID를 사용하고,per_team은 키의 조직 ID를 사용합니다. 허브에서 동기화된 키는 동기화된 항목의user_id와team_id를 쓰며, 정적 키는api_keys의user_id와organization_id필드를 씁니다. - 해석되지 않은 식별자: 키에 사용자 ID가 없거나 팀 ID가 없으면 해당 요청은 설정된 차원을 건너뜁니다. 여기에는 익명 호출자, 식별자 없이 설정된 정적 키, 동기화된
team_id가 없는 허브 키(continuum-hub자리표시자는 팀 식별자가 아님)가 포함됩니다. 건너뛴 횟수는rate_limit_skipped_total{dimension="user"}또는rate_limit_skipped_total{dimension="team"}에 기록됩니다. 이러한 요청은 다른 식별자 없는 키와 공유하는 버킷에 절대 청구되지 않습니다. - 거절과 메트릭: 거절 시
error.details.limit_type에 거절한 차원(user또는team)이 기록되고,rate_limit_exceeded_total{limit_type="user"}또는rate_limit_exceeded_total{limit_type="team"}으로 집계됩니다. 두 차원 모두 둘 이상의 자격 증명이 공유하므로 버킷 잔량은 응답 헤더에 노출하지 않습니다. - 스토리지와 재적용: 인메모리 버킷 맵과 Redis 키 공간(
per_user:<hash>,per_team:<hash>)은 식별자의 레플리카 간 안정적인 해시를 사용하므로 모든 레플리카가 하나의 버킷을 공유합니다. 설정을 다시 적용하면 다른 차원과 마찬가지로 새 설정이 즉시 반영되고 테넌트 버킷이 초기화됩니다. - 토큰 예산:
tokens_per_minute와tokens_per_day는 다른 차원과 같은 예약 수명 주기로 동작합니다. 사용자별 예산은 해당 사용자의 모든 키가, 팀별 예산은 팀의 모든 구성원이 공유합니다. 거절 시token_dimension에user또는team이 기록되며 버킷 잔량은 숨깁니다.
rate_limiting:
limits:
per_user:
requests_per_second: 20
burst_capacity: 40
tokens_per_minute: 200000 # 사용자의 모든 키가 공유합니다.
per_team:
requests_per_second: 100
burst_capacity: 200
tokens_per_day: 50000000 # 팀의 모든 구성원이 공유합니다.
동시 처리 상한¶
max_in_flight_requests는 한 테넌트가 동시에 유지하는 요청 수를 제한합니다. 요청 수 제한만으로는 표현할 수 없는 값입니다. 예를 들어, 30초짜리 생성을 초당 10건 실행하면 약 300개의 스트림이 동시에 열려 있습니다. per_api_key, per_user, per_team, per_client의 다른 설정 옆에 지정하며, 기본값 0은 해당 차원의 동시 처리 제한을 두지 않습니다.
- 수락 시점: 요청 수와 토큰 예산 검사를 모두 통과한 뒤 슬롯을 잡으므로, 거부된 요청은 슬롯을 차지하지 않습니다. 상한을 넘는 요청은 즉시
429로 거부하며, 큐에 넣지 않습니다. - 거부 형식:
limit_type: "inflight",api_key,user,team,client중 하나를 담는error.details.inflight_dimension, 그리고Retry-After: 1을 반환합니다. 라우터는 스트림이 언제 끝날지 예측하지 않으므로, 더 긴 대기 시간을 안내하지 않습니다. - 해제: 슬롯은 응답 본문에 붙는 RAII 가드입니다. 스트림은 마지막 프레임까지 슬롯을 유지하며, 핸들러 실패와 클라이언트 연결 종료는 요청 future와 함께 슬롯을 해제합니다.
- 무시되는 차원:
global,per_backend,per_model입니다. 라우터 전체 상주 한도는server.max_concurrent_requests를 사용하며, 앞의 세 차원에 값을 지정하면config validate가 경고합니다. - 회계 단위: 인메모리 요청 버킷과 같이 레플리카별입니다. 따라서 N개 레플리카 함대는 설정 상한의 최대 N배까지 허용하며, Redis는 이 카운트를 공유하지 않습니다.
- 우회: 전체
whitelist또는bypass_keys예외는 상한을 건너뛰고,inflight를 지정한scoped_bypass_keys항목은 이 클래스만 건너뜁니다. - 메트릭:
rate_limit_inflight_requests{dimension, tenant}는 현재 점유 수를 보고하고 0이 되면 제거됩니다.rate_limit_inflight_refusals_total{dimension, tenant}는 거부 횟수를 집계하며, 거부는rate_limit_exceeded_total{limit_type="inflight"}에도 함께 집계됩니다.
rate_limiting:
enabled: true
limits:
per_user:
requests_per_second: 20
burst_capacity: 40
max_in_flight_requests: 64 # 사용자의 모든 키가 동시에 유지하는 스트림 수
per_team:
requests_per_second: 100
burst_capacity: 200
max_in_flight_requests: 512 # 팀 전체가 동시에 유지하는 스트림 수
등급별 속도 제한¶
허브 등급마다 키별 제한을 따로 둘 수 있습니다. 따라서 유료 등급에는 무료 등급보다 큰 요청 속도와 버스트를 주면서도 모든 키에 적용되는 limits.per_api_key는 그대로 둘 수 있습니다:
rate_limiting:
limits:
per_api_key:
requests_per_second: 10
burst_capacity: 20
tokens_per_minute: 100000
tier_overrides:
tier_premium:
requests_per_second: 50
burst_capacity: 100
tokens_per_minute: 500000
tier_free:
requests_per_second: 2
burst_capacity: 4
- 해석: 제한기는 제시된 자격 증명을 허브에서 동기화된 키 테이블과 대조해 키의
tier_id를 읽습니다.smart_routing.routing_policies[].when.key_tier가 가리키는 것과 같은 식별자입니다. 이 동작에는control-plane빌드 기능,control_plane.enabled: true, 정책 집행, 동기화된 정책이 모두 필요합니다. 이것이 없으면 맵은 아무 효과가 없고 모든 키는 이 맵이 없을 때의 설정을 그대로 쓰며,config validate와 시작 로그가 경고합니다. - 항목이 바꾸는 값:
requests_per_second와burst_capacity는 항상 해당 등급의 키에 적용됩니다.tokens_per_minute는 값을 적었을 때만per_api_key의 분당 예산을 바꾸며, 생략하면 차원의 예산을 물려받습니다. 일일 예산은 오버라이드 대상이 아니며 설정된 값을 유지합니다. - 우선순위: 명시적인
api_keys[].rate_limit(rate_limit_burst,rate_limit_tokens_per_minute포함)이 가장 구체적인 설정이므로 등급보다 우선합니다. 등급은limits.per_api_key보다 우선합니다. - 버킷과 헤더: 해석된 등급은 요청 속도 버킷 키와 분당 예산 버킷 키에 포함됩니다. 따라서 허브가 키를 다른 등급으로 옮기면 이전 용량을 재사용하지 않고 새 등급의 버킷에서 시작합니다.
x-ratelimit-*헤더는 해당 요청에 실제로 적용된 제한을 보고합니다. - 허브 분당 한도와의 관계:
tier_overrides는 로컬 제한만 설정합니다. 허브 등급의rpm,input_tpm,output_tpm윈도(공유 카운터를 쓰는control_plane.policy.minute_limits포함)는 컨트롤 플레인 경로가 따로 집행합니다. 로컬 오버라이드는 이 윈도를 대체하거나 넓히지 않으며, 요청은 로컬 제한기와 허브 윈도가 모두 허용할 때만 통과합니다. - 검증:
requests_per_second와burst_capacity는 필수이며 0보다 커야 하고,burst_capacity는requests_per_second이상이어야 합니다. 항목 안의 알 수 없는 필드는 로드 오류입니다. 각 등급 ID는 비어 있지 않아야 하며 최대 128바이트입니다.
우회 설정¶
특정 클라이언트는 속도 제한을 완전히 우회할 수 있습니다:
rate_limiting:
# 속도 제한을 우회하는 화이트리스트 IP
whitelist:
- "192.168.1.0/24"
- "10.0.0.1"
# 모든 속도 제한 차원을 우회하는 API 키
bypass_keys:
- "admin-key-123"
- "monitoring-key-456"
bypass_keys와 whitelist는 호출자를 모든 차원에서 면제합니다. 대신 특정 차원 부류만 면제하려면 scoped_bypass_keys 항목을 추가합니다:
rate_limiting:
scoped_bypass_keys:
# 신뢰하는 게이트웨이: 요청 수 제한 없음, 토큰 예산은 계속 적용.
- key: "gateway-key-789"
dimensions: [request_rate]
# 배치 가져오기: 토큰 속도 제한 면제, 요청 수 제한은 계속 적용.
- key: "batch-key-012"
dimensions: [tokens_per_minute, tokens_per_day]
dimensions 목록은 다음 값을 받습니다.
request_rate: 요청 수 버킷global,per_client,per_api_key,per_model.tokens_per_minute: 모든 수락 시점 차원의 분당 토큰 예산.tokens_per_day: 모든 수락 시점 차원의 일일 토큰 예산.
항목은 지정한 부류만 면제하므로, 지정하지 않은 차원은 계속 해당 키에 과금합니다. 같은 키에 대한 bypass_keys 항목이 있으면 여전히 모든 차원을 면제합니다. 일치하는 요청은 각각 rate_limit_bypassed_requests_total로 집계되며, 키 값은 설정 내보내기에서 마스킹됩니다.
클라이언트 식별¶
라우터는 다음 우선순위로 클라이언트를 식별합니다:
-
API 키 -
Authorization: Bearer <token>또는x-api-key헤더 (권장)- IP가 바뀌어도 동일한 클라이언트를 정확하게 추적합니다.
-
X-Forwarded-For 헤더 (프록시/로드 밸런서 환경)
- 신뢰할 수 있는 프록시에서 전달된 요청에만 적용됩니다.
use_rightmost_xff: true(기본값)이면 체인의 가장 오른쪽 IP를 사용하여 헤더 위조를 방지합니다.
-
X-Real-IP 헤더 (대체 프록시 헤더)
- 마찬가지로 신뢰할 수 있는 프록시에서 전달된 요청에만 적용됩니다.
-
직접 연결 IP 주소 (프록시 헤더가 없는 경우)
- 요청이 라우터에 직접 도달할 때 사용됩니다.
신뢰 프록시 설정¶
rate_limiting:
# X-Forwarded-For / X-Real-IP 헤더 설정이 허용되는 프록시
trusted_proxies:
- "10.0.0.0/8"
- "192.168.1.1"
# X-Forwarded-For 체인에서 가장 오른쪽 IP 사용 (기본값: true)
use_rightmost_xff: true
신뢰 목록에 없는 출처가 보낸 포워딩 헤더는 무시되므로, 클라이언트가 X-Forwarded-For를 위조해 클라이언트별 제한을 회피할 수 없습니다.
Unix 소켓 리스너¶
Unix 도메인 소켓 피어에는 IP 주소가 없으므로, unix: 리스너에서는 클라이언트별 차원과 IP 화이트리스트를 적용하지 않고 건너뜁니다. 건너뛸 때마다 rate_limit_skipped_total{dimension="client"} 메트릭과 디버그 로그에 기록되며, 전역 제한과 API 키별 제한은 정상적으로 집행됩니다.
속도 제한 전략¶
토큰 버킷 알고리즘¶
라우터는 장기 평균 속도를 제한하면서 단기 버스트 트래픽을 허용하는 토큰 버킷 알고리즘을 사용합니다:
- 버킷 용량: 최대 토큰 수 (
burst_capacity) - 리필 속도: 초당 충전되는 토큰 수 (
requests_per_second) - 토큰 비용: 요청마다 토큰 1개 소비
작동 방식¶
- 각 클라이언트는 토큰이 가득 찬 버킷으로 시작합니다.
- 요청이 처리될 때마다 토큰이 소비됩니다.
- 토큰은 일정한 속도로 다시 채워집니다.
- 버킷이 비어 있으면 요청이 거부됩니다.
백엔드별 제한¶
limits.per_backend는 호출자 할당량이 아니라 개별 백엔드의 용량 한도이므로, 요청 진입 시점의 거절이 아니라 라우팅 필터로 동작합니다:
- 후보 필터링: 요청한 모델을 여러 백엔드가 서빙할 때, 백엔드별 버킷이 빈 백엔드는 라우팅 선택 전략이 실행되기 전에 후보군에서 제외되어 여유 용량이 있는 백엔드로 요청이 전달됩니다. 이 필터는 모든 요청 경로(chat, 스트리밍, Responses, Anthropic Messages, 이미지, realtime, ACP)에서 헬스 필터 및 서킷 브레이커 필터와 함께 실행됩니다.
- 디스패치 시점 차감: 버킷은 요청이 실제로 전송되는 백엔드에 대해, 디스패치되는 순간 토큰 1개만 차감됩니다. 필터에서 제외된 백엔드, 선택 전략이 고르지 않은 백엔드, 폴백 홉에서 건너뛴 백엔드, 캐시 히트, 가드레일이 차단한 요청은 차감되지 않습니다. 유입 경로와 무관하게 디스패치 1회당 토큰 1개가 차감됩니다.
- 모든 백엔드 소진 시에만 거절: 모델을 서빙하는 모든 후보 백엔드의 버킷이 비어 있으면, 장애 시 반환되는
503 service_unavailable과 구분되도록error.details.limit_type이backend인429를 반환합니다. 폴백 체인이 설정되어 있으면 이429가 기본error_codes트리거와 일치하므로, 클라이언트에 오류를 반환하기 전에 여유 용량이 있는 대체 모델로 폴백합니다. - 기본값 비활성화:
per_api_key와 마찬가지로 기본 설정에서는 꺼져 있으며, 명시적으로 설정한 경우에만 동작합니다. - 우회 설정 및 Redis 연동:
whitelist와bypass_keys는 진입 계층의 제한만 면제하며 백엔드의 용량 한도는 면제하지 않습니다. 따라서 면제 대상 호출자도 용량이 소진된 백엔드를 우회하여 라우팅되며, 모든 백엔드가 소진되면 동일하게429를 받습니다.storage: redis를 사용하면 백엔드별 버킷도 다른 차원과 마찬가지로 라우터 복제본 간에 공유되며, Redis 장애 시에는 저장소 장애 정책을 따릅니다.redis.fallback_to_memory: false에서는 용량 소진을 뜻하는429가 아니라503 service_unavailable과Retry-After: 5로 요청을 거절하고 폴백 체인도 시작하지 않습니다.
rate_limiting:
limits:
per_backend:
requests_per_second: 50 # 백엔드당 정상 상태 초당 디스패치 수
burst_capacity: 100 # 백엔드가 한 번에 흡수할 수 있는 최대 디스패치 수
모델별 제한¶
클라이언트별, 백엔드별, API 키별, 전역 차원 외에도 개별 모델 단위로 속도 제한을 설정할 수 있습니다. 저비용 모델은 열어 두고 고비용 모델만 보호할 때 유용합니다:
rate_limiting:
limits:
per_model:
gpt-5.6-sol:
requests_per_second: 2
burst_capacity: 5
gpt-5.6-luna:
requests_per_second: 20
burst_capacity: 50
버킷을 고르는 이름¶
버킷 키는 요청에 적힌 클라이언트 대면 모델 이름을 그대로 사용하며, 대소문자 변환이나 공백 제거 같은 정규화를 수행하지 않고 정확히 일치하는 문자열만 매칭합니다. 클라이언트가 전송하는 모델 이름과 다르게 per_model 키를 설정하면 해당 버킷은 차감되지 않습니다.
라우터는 요청 경로 또는 본문에서만 모델 이름을 추출하며, X-Model 헤더는 모든 경로에서 무시합니다. 핸들러 내부 라우팅보다 먼저 평가되므로 다음 두 가지 규칙이 적용됩니다:
model_aliases: 모델 별칭은 속도 제한 평가가 끝난 뒤 핸들러 내부에서 치환되므로, 버킷은 클라이언트가 요청한 원래 이름을 기준으로 동작합니다. 별칭의 대상 모델에 설정한 제한은 그 대상 이름을 직접 지정한 요청에만 적용됩니다. 하나의 업스트림 모델에 두 개의 클라이언트 대면 이름으로 접근할 수 있다면 각 이름마다per_model항목을 등록해야 합니다.model_experiments: 실험 변형(variant) 배정 또한 속도 제한 평가 이후에 수행되므로, 실험의 클라이언트 대면model이름에 설정한 제한은 요청이 어느 변형으로 배정되든 실험 전체 트래픽을 묶어서 제한합니다. 개별 변형 모델 이름에 설정한 제한은 클라이언트가 그 변형 이름을 직접 지정한 요청에만 적용됩니다.
적용되는 경로¶
per_model은 요청 경로나 JSON 본문에서 모델 이름을 제공하는 경로에만 적용됩니다:
- 요청 경로: 모델 조회 엔드포인트
GET /v1/models/{model}(vendor/model형태의 네임스페이스 ID 포함). - JSON 요청 본문의 최상위
model필드:POST /v1/chat/completions,/v1/completions,/v1/embeddings,/v1/rerank,/embed_sparse,/v1/responses,/v1/responses/compact,/v1/images/generations,/v1/audio/speech,/anthropic/v1/messages와 루트 수준 별칭/v1/messages. 진입 계층은 기존 10 MB 요청 본문 한도 내에서 본문을 한 번 확인한 뒤 변경 없이 핸들러로 전달합니다.
그 외의 경로는 per_model 대상이 아니며, global, per_client, per_api_key 차원으로 제한됩니다:
/v1/images/edits및/v1/images/variations: multipart 폼 본문을 진입 계층이 아닌 후속 단계에서 파싱합니다./anthropic/v1/messages/count_tokens와/v1/messages/count_tokens: 토큰 수만 계산하며 모델 생성을 수행하지 않습니다./tokenize,/detokenize및 각/v1별칭: 엔진 토크나이저만 호출하며 생성을 수행하지 않습니다.- 모델 매개변수가 없는 경로:
GET /v1/models,/v1/models/extended,/v1/models/proxy,/v1/batches,/v1/responses/{id}, realtime WebSocket 엔드포인트.
적용 대상 경로라도 본문에 model 필드가 없거나, 유효한 JSON이 아니거나, 본문 검사 한도를 초과하는 요청은 per_model에서 차감하지 않습니다. 이러한 요청은 그대로 핸들러에 전달되어 일반 검증 및 오류 응답 절차를 거치며, 건너뛴 평가는 rate_limit_skipped_total{dimension="model"} 메트릭에 집계됩니다.
토큰 예산¶
요청 수 차원은 요청이 몇 개 도착하는지만 제한하고 요청 하나가 얼마나 큰 작업을 나르는지는 보지 않습니다. 20만 토큰 프롬프트를 담은 요청 하나나 thinking 예산을 최대로 잡은 추론 모델은 어떤 요청 속도 상한 아래에서도 백엔드를 수 분간 점유합니다. requests_per_second 옆에 tokens_per_minute, 필요하면 tokens_per_day를 설정하면 같은 차원에서 토큰까지 제한할 수 있습니다:
rate_limiting:
limits:
per_api_key:
requests_per_second: 10
burst_capacity: 20
tokens_per_minute: 100000
tokens_per_day: 2000000
global:
requests_per_second: 1000
burst_capacity: 2000
tokens_per_minute: 5000000
tokens_per_day: 50000000
- 수락 시점 차원에만 적용됩니다:
per_api_key,per_user,per_team,per_client,per_model,global이 각자 독립된 토큰 버킷을 가집니다. 요청은 신원이 있는 설정 차원마다 과금됩니다. 키 식별자, 키의 사용자 식별자, 키의 팀 식별자, 피어 주소, 클라이언트가 지정한 모델 이름과 일치하는per_model항목(다른 모델 항목은 적용되지 않음), 그리고 플릿 전체global버킷입니다.per_backend는 선택 시점에 후보를 거르는 필터이고 디스패치마다 한 번 과금되므로 토큰 예산을 붙일 지점이 없습니다. 값을 설정해도 무시되며config validate가 경고합니다. - 한 번에 함께 예약됩니다: 설정된 토큰 차원은 인증 안쪽, 디스패치 이전의 식별 계층에서 한 번에 예약하므로 요청은 차원마다 정확히 한 번 과금됩니다.
global과per_client의 요청 수 한도는 여전히 각자의 바깥 마운트에서 집행됩니다. - 버스트는 1분치입니다: 버킷 용량은
tokens_per_minute이고 초당tokens_per_minute / 60으로 보충되므로, 호출자는 자기 몫의 1분치를 한꺼번에 쓸 수 있습니다. 기본값0은 그 차원을 토큰 무제한으로 두며 필드가 없는 설정은 이전과 똑같이 동작합니다. - 선택적 일일 예산:
tokens_per_day는 같은 차원에 분 버킷 옆의 두 번째 독립 버킷을 추가하며, 같은 예약과 재조정으로 과금합니다. 요청은 두 예산을 모두 감당해야 합니다. 용량은 속도의 1일치이고 초당tokens_per_day / 86400으로 보충되므로, 호출자는 자기 몫의 1일치를 한꺼번에 쓸 수 있고 다시 채워지는 데는 소진 뒤 1일이 걸립니다. 누출 버킷(leaky-bucket) 윈도이며 자정에 리셋되는 캘린더 데이 방식이 아닙니다. - 수락 시점에는 과대 추정치를 예약합니다: 디스패치 전에 전체 요청 입력 추정치와 출력 한도를 요청에 적용되는 모든 차원에 예약합니다. 출력 한도는
max_tokens,max_completion_tokens,max_output_tokens중 가장 큰 값이고 셋 다 없으면 4096입니다.32000.0이나"32000"처럼 소수나 문자열로 적은 값도 같은 상한으로 읽습니다. pydantic 기반 업스트림이 이를 정수로 바꿔 그만큼 생성하기 때문입니다. 상한을 밝히지 않은 요청이 작은 입력만 잡고 무한히 생성하는 것을 막기 위해서입니다. 버킷 전체보다 큰 요청은 기다려도 들어갈 수 없으므로 즉시 거부합니다. - 크기 인식 거절: 토큰 예산 거부의
Retry-After는 고정된 윈도가 아니라, 소진된 예산이 거부된 요청 자체의 비용을 다시 채우는 데 필요한 시간을 알려 줍니다. 같은 예산이 거부한 10 토큰 요청과 200k 토큰 요청은 서로 다른 대기 시간을 받습니다. 힌트는 최소 1초, 최대 86400초입니다. - 지연 수락 (기본값 꺼짐):
rate_limiting.deferred_admission.enabled를 설정하면 대기 시간이max_wait_ms(기본값 2000)에 맞는 거부 요청을 429 대신 그 시간 동안 보류합니다. 보류는 라우터 프로세스마다max_held_requests(기본값 1000)로 경계가 정해지며, 이 카운트는 복제본마다 별도로 유지됩니다. 어느 경계든 넘는 요청은 크기 인식Retry-After와 함께 즉시 429를 받습니다. 보류된 요청은 아직 디스패치되지 않았으므로 백엔드 슬롯도 토큰 예약도 차지하지 않고, 예약은 대기가 성공한 뒤에 잡힙니다. 버스트 전체보다 큰 요청은 기다려도 들어갈 수 없으므로 보류하지 않습니다. - 테넌트 간 공정 큐잉 (기본값 꺼짐):
rate_limiting.fair_queuing.enabled를 설정하면 식별자 차원(per_api_key,per_user,per_team,per_model)이 거부한 요청을 벌거벗은 429 대신 해당 테넌트의 경계가 있는 큐에 보류하고, 큐는 가중 공정 스케줄링으로 처리합니다. 따라서 한 테넌트의 홍수가 다른 테넌트를 굶기지 못합니다. 테넌트는 리미터가 과금하는 사용자, 키, 팀 식별자와 같고(tenant_dimension, 기본값user),weights로 테넌트별 몫을 조정합니다. 모든 수준에 경계가 있습니다(max_queue_depth_per_tenant32,max_total_queued4096,max_tenants1024,max_in_flight8,max_wait_ms5000). 경계에 맞지 않거나 대기가 예산에 맞지 않는 요청은x-fair-queue-position과x-fair-queue-wait-ms와 함께 거부됩니다. 대기 시간은 요청의 전체 타임아웃에 포함됩니다. 전역 및 클라이언트별 거부는 해당 마운트가 인증 전에 실행되므로 즉시 처리되고,deferred_admission은 토큰 예산 거부를 계속 담당하므로 두 기능을 함께 켤 수 있습니다. 새 메트릭은rate_limit_fair_queue_depth,rate_limit_fair_queue_tenants,rate_limit_fair_queue_wait_seconds{tenant_dimension},rate_limit_fair_queue_grants_total{tenant_dimension},rate_limit_fair_queue_refused_total{tenant_dimension, reason}입니다. 자세한 내용은 테넌트 간 공정 큐잉을 참고하세요. - 완료 시점에 보고된 사용량으로 재조정합니다: 남은 예약은 환급하고 초과분은 추가로 과금합니다. 실패, 사용량 블록이 없는 응답, 클라이언트 취소는 예약 전체를 해제합니다. 스트리밍 응답은 스트림이 정상적으로 끝났을 때 같은 방식으로 정산하며, 백엔드가 사용량을 보고하지 않으면 라우터가 센 값으로 정산합니다. 끝나기 전에 끊긴 스트림(취소, 타임아웃, 업스트림 오류, 가드레일 차단)은 예약 전체를 해제합니다.
- 텍스트 생성 경로만 과금합니다:
/v1/chat/completions,/v1/completions,/v1/responses,/v1/responses/compact,/v1/messages,/anthropic/v1/messages입니다. 나머지 경로는 요청 수 차원만 적용받습니다.
입력 추정치는 모든 메시지, tools, functions, response_format을 훑고 채팅 본문 순회가 보지 못하는 프롬프트 필드(prompt, input, instructions, system)를 더합니다. 그래서 completions, Responses, Anthropic 형식도 자기 입력만큼 과금됩니다. 본문을 읽지 못하면 이 요청은 아무것도 예약하지 않고 그대로 허용되며 본문 검증과 오류 형식은 여전히 라우트 핸들러가 맡습니다.
요청에 신원이 없는 차원은 건너뛰고 rate_limit_skipped_total{dimension="token_budget_<차원>"}으로 집계합니다. per_api_key는 인증된 신원이, per_user는 해석된 사용자 식별자가, per_team은 해석된 팀 식별자가, per_client는 피어 주소가, per_model은 해석된 모델 이름이 필요합니다. 건너뛴 차원은 아무것도 과금하지 않으므로, 토큰 예산을 per_api_key에만 둔 배포는 신원을 붙일 수 없는 트래픽에 토큰을 하나도 부과하지 않습니다. 그 트래픽을 막는 것은 함께 설정한 공유 차원입니다. 화이트리스트와 bypass_keys는 다른 리미터 차원과 마찬가지로 토큰 예산에서도 요청을 면제합니다. scoped_bypass_keys 항목은 지정한 기간만 면제하므로, request_rate만 면제한 키는 토큰 예산을 유지하고 tokens_per_minute만 면제한 키는 일일 예산을 유지합니다.
tokens_per_minute는 0부터 100000000까지, tokens_per_day는 0부터 100000000000까지의 정수를 받으며 0은 무제한입니다. 허브에서 동기화된 키 경로는 자체 input_tpm/output_tpm 윈도와 control_plane.policy.minute_limits 공유를 적용하며 여기서 다루는 로컬 차원은 그 경로와 독립적입니다. 허브에 바인딩된 키의 요청은 두 경로의 제한을 모두 받을 수 있습니다. 설계 배경과 결정 사항은 토큰-분당·일일 예산에 있습니다.
배포 전체 토큰 예산¶
storage: memory에서는 복제본마다 자체 토큰 버킷을 운영하므로, 복제본 N개짜리 배포는 설정한 tokens_per_minute / tokens_per_day의 최대 N배까지 허용합니다. 설정값이 배포 전체 기준으로 동작하려면 라우터가 그 예산을 나눠 갖는 복제본 수를 알아야 합니다. 다음 네 가지 모드가 이를 다룹니다:
| 모드 | 적용 단위 | 나눗셈 설정 방법 |
|---|---|---|
| 인메모리 복제본별 (기본값) | 복제본마다 하나의 예산 | 없음. 이 상태로 토큰 예산을 설정하면 config validate가 경고합니다 |
| 설정 힌트 분할 | 복제본마다 설정값 / replicas | rate_limiting.budget_division.replicas |
| 허브 주도 분할 | 복제본마다 허브가 밀어 준 몫 | 허브 정책의 deployment_token_budget 멤버 |
| Redis 공유 | 배포 전체가 공유하는 하나의 예산 | 연결 가능한 엔드포인트를 지정한 storage: redis |
rate_limiting:
storage: memory
budget_division:
replicas: 4
limits:
global:
requests_per_second: 1000
burst_capacity: 2000
tokens_per_minute: 4000000
tokens_per_day: 40000000
- 수락 시점 토큰 차원마다 나눕니다:
global,per_client,per_api_key,per_user,per_team, 그리고 각per_model항목이 기간별로설정값 / replicas를 정수 내림으로 가집니다.per_backend는 토큰 차원이 아니므로 그대로입니다. 예약, 거부, 정산 경로는 바뀌지 않습니다. - 양수 예산이 무제한이 되지는 않습니다: 플릿이 예산보다 크면 기간별 용량이 0으로 내림됩니다. 이는 양수 예약을 모두 거부하는 확정적 0 허용량이며,
0 = 무제한설정 관례와 다릅니다. - 허브 몫이 기간별로 우선합니다:
share_tokens_per_minute와share_tokens_per_day는 해당 기간의 분할된 용량을 대체하고, 문장에 없는 기간은 로컬 힌트를 유지합니다. 따라서 한 기간은 허브가 제어하고 다른 기간은 설정 힌트를 따를 수 있습니다. 멤버가 없거나 두 몫이 모두 없는 문장(확정적 해제)이면 모든 기간이 로컬 힌트로 돌아갑니다. 밀어 준0은 해제가 아니라 0 허용량입니다. 라우터는 밀어 준 몫을 그대로 적용하며 배포 전체 값과 등록된 라우터 수로 몫을 다시 계산하지 않습니다. - Redis는 설정 힌트를 무시합니다: 공유 Redis 버킷이 이미 배포 전체 값을 담고 있으므로 Redis 백엔드를 쓰는 동안
budget_division은 적용되지 않고, 둘 다 설정하면config validate가 경고합니다. 허브 몫은 존재하는 동안 그대로 적용됩니다. - 핫 리로드:
budget_division.replicas가 바뀌거나 허브가 다른 몫을 밀어 주면 예산을 다시 계산하고 토큰 버킷 맵을 초기화하므로, 새 용량은 다음 윈도부터 적용됩니다. 이미 처리 중인 요청은 자신이 예약한 파라미터로 정산합니다. - 검증: 인메모리 백엔드에 0이 아닌 수락 시점 토큰 예산이 있는데
budget_division이 없으면config validate가 경고합니다. 이때 실제 한도는 복제본별이기 때문입니다.replicas는 1 이상이어야 하며 1은 정상적인 무변화 값입니다.
Files API 작업 유형별 제한¶
라우터 전체의 rate_limiting 설정과 별도로, /v1/files 엔드포인트는 files.rate_limiting 아래에서 작업 유형별·호출자별 속도 제한을 지원합니다:
files:
enabled: true
rate_limiting:
enabled: true
uploads_per_minute: 10 # POST /v1/files (기본값: 10)
downloads_per_minute: 100 # GET /v1/files/{file_id}/content (기본값: 100)
operations_per_minute: 1000 # GET /v1/files, GET/HEAD/DELETE /v1/files/{file_id} (기본값: 1000)
files.rate_limiting.enabled가 true이면 호출자마다 세 개의 독립된 토큰 버킷이 적용됩니다:
- 유효성 검증: 세 한도(
uploads_per_minute,downloads_per_minute,operations_per_minute) 모두0보다 커야 하며, 시작 시점과 핫 리로드 시점에 검증됩니다. - 호출자별 독립 버킷: 제시된 API 키(
Authorization: Bearer또는x-api-key)의 SHA-256 해시, 인증된 사용자 ID, 또는 클라이언트 IP를 키로 하여files_upload,files_download,files_operations버킷이 각각 생성됩니다. - 응답 헤더 및
429처리: 모든 Files API 응답에는x-ratelimit-limit,x-ratelimit-remaining,x-ratelimit-reset과 각-requests변형 헤더가 포함됩니다. 한도를 초과하면Retry-After헤더와 함께error.details.limit_type에 해당 범주 이름(files_upload,files_download,files_operations)을 담은429 Too Many Requests를 반환합니다.
응답 헤더¶
성공 응답¶
요청이 속도 제한 이내이고 호출자 범위 차원(per_api_key 또는 per_client)이 요청을 평가한 경우:
이 숫자는 호출자 본인의 버킷 상태를 나타냅니다. 공유 차원(global, per_user, per_team, per_model, per_backend)만 설정된 경우에는 버킷 잔량이 개별 호출자의 할당량이 아닌 공유 부하를 나타내므로, 허용된 응답에 X-RateLimit-* 헤더가 포함되지 않습니다. 자세한 규칙은 헤더 귀속과 거절 우선순위를 참고하십시오.
per_api_key 토큰 예산은 별도 헤더 계열인 X-RateLimit-Tokens-Limit, X-RateLimit-Tokens-Remaining, X-RateLimit-Tokens-Reset을 추가합니다. 값은 버킷 용량, 이 요청의 예약 뒤에 남은 토큰, 버킷이 다시 가득 차는 Unix 시각입니다. per_api_key 일일 예산은 자기 버킷의 X-RateLimit-Tokens-Day-* 변형 헤더를 추가합니다. 두 버킷이 독립적이므로 토큰 계열을 요청 수 헤더와 따로 둡니다. 요청 수 한도에 가까우면서 토큰 예산은 거의 빈 상태일 수도 있고 그 반대일 수도 있습니다. per_client, per_user, per_team, per_model, global의 토큰 예산은 해당 요청 수 잔량을 숨기는 것과 같은 이유로 값을 숨깁니다.
속도 제한 초과 응답¶
속도 제한을 초과했을 때 반환되는 응답은 다음과 같습니다:
HTTP/1.1 429 Too Many Requests
Retry-After: 30
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1640995230
Content-Type: application/json
{
"error": {
"message": "Rate limit exceeded. Try again in 30 seconds.",
"type": "rate_limit_exceeded",
"param": null,
"code": "rate_limit_exceeded",
"details": {
"limit_type": "api_key",
"limit": 20,
"remaining": 0,
"reset_time": 1640995230
}
}
}
429 응답의 주요 동작 규칙은 다음과 같습니다:
-
Anthropic 오류 형식:
/anthropic/*경로와 루트 수준/v1/messages별칭에서는 본문이 Anthropic 오류 형식을 따르므로, Anthropic SDK가 이 응답을 rate limit 오류로 인식합니다. 상태 코드, 헤더,error.details는 위와 같습니다: -
Retry-After최소값 보장:Retry-After는 항상1이상입니다 (0을 반환하지 않음). 방금 거절된 엔드포인트에 클라이언트가 대기 없이 즉시 재시도하는 것을 방지합니다. error.details.limit_type: 요청을 차단한 차원(global,client,api_key,user,team,backend,model:<이름>,token_budget)을 표시합니다. 여러 진입 계층이 동시에 걸릴 때 어느 차원의 값이 우선하는지는 헤더 귀속과 거절 우선순위를 참고하십시오.- 토큰 예산 거부 (
limit_type: "token_budget"): 소진된 토큰 버킷은 올려야 할 예산을error.details.token_dimension(global,client,api_key,user,team,model)으로 밝힙니다.per_api_key거부는 그 버킷의limit과remaining을 본문과x-ratelimit-tokens-*에 함께 담고,global,client,user,team,model거부는 둘 이상의 자격 증명이 그 버킷을 쓰므로 두 값을 숨깁니다.Retry-After는 크기를 반영해, 소진된 예산이 거부된 요청 자체의 비용을 다시 채우는 데 필요한 시간을 알려 줍니다. 토큰 검사가 Redis에서 실행되는데 Redis에 닿지 못하고rate_limiting.redis.fallback_to_memory: false이면, 요청 수 차원과 마찬가지로Retry-After: 5와 함께503을 반환합니다. - 백엔드 한도 초과 (
limit_type: "backend"): 진입 계층이 아니라 라우팅 계층에서 모든 후보 백엔드가 용량 한도에 도달했을 때 반환합니다 (백엔드별 제한 참고). 동일한 JSON 오류 본문과Retry-After를 포함하지만, 소진된 버킷이 개별 호출자가 아닌 백엔드 플릿의 용량이므로X-RateLimit-*헤더는 생략합니다. - 브라우저 및 CORS 처리: 식별 차원의
429응답은 CORS 계층 안쪽에서 생성되어Access-Control-Allow-Origin헤더를 포함합니다. 반면 전역 장착 지점은 CORS 계층 바깥에 위치하므로,Access-Control-Allow-Origin이 없는429는 브라우저에서 상태 코드를 읽을 수 없는 불투명한 네트워크 오류로 처리됩니다.
모니터링¶
메트릭¶
속도 제한 동작은 Prometheus 메트릭으로 추적됩니다:
# 수행된 총 속도 제한 검사 수
rate_limit_checks_total 10342
# 거부된 요청 (제한 차원별 레이블)
rate_limit_exceeded_total{limit_type="per_client"} 42
# 현재 토큰 버킷 잔량과 용량
rate_limit_tokens_remaining{bucket_type="per_client",identifier="..."} 15
rate_limit_bucket_capacity{bucket_type="per_client"} 20
# 화이트리스트 및 우회(API 키) 요청
rate_limit_whitelisted_requests_total 123
rate_limit_bypassed_requests_total 45
# 요청에 해당 식별 정보가 없어 건너뛴 차원 (예: Unix 소켓 리스너의
# 클라이언트별 차원, 익명 호출자의 키별 차원). 이 카운터가 꾸준히 증가한다면
# 설정한 제한이 실제로 적용되지 않고 있다는 신호입니다.
rate_limit_skipped_total{dimension="client"} 7
# 토큰 예산(tokens_per_minute) 활동. outcome은 reserved, refunded, charged이며,
# 거부는 rate_limit_exceeded_total{limit_type="token_budget"}에도 함께 집계됩니다.
rate_limit_token_budget_tokens_total{dimension="api_key",outcome="reserved"} 120000
rate_limit_token_budget_refusals_total{dimension="global"} 2
# 동시 처리 상한: 테넌트별 현재 점유 수(0이 되면 시리즈가 제거됨)와 거부 횟수이며,
# 거부는 rate_limit_exceeded_total{limit_type="inflight"}에도 함께 집계됩니다.
rate_limit_inflight_requests{dimension="user",tenant="..."} 3
rate_limit_inflight_refusals_total{dimension="user",tenant="..."} 1
# 공유 저장소(Redis)에 연결할 수 없었던 Redis 기반 검사 (storage: redis).
# action="fallback": 이 레플리카의 버킷으로 집행 (fail-open, 기본값)
# action="refused": 503으로 거부 (redis.fallback_to_memory: false)
rate_limit_redis_unavailable_total{dimension="global",action="fallback"} 3
레플리카 간 메트릭 집계¶
속도 제한 계열과 데이터 플레인 예산 계열(router_budget_*, 예산 참고)은 집계 방식이 네 가지로 나뉘며, 어느 분류인지에 따라 레플리카 간 sum이 올바른지 결정됩니다. 레플리카별 집행 카운터는 이 레플리카가 판정하는 지점에서 증가하므로 함대 전체 값은 합계이고, rate()/increase() 윈도가 재시작과 스케일 이벤트를 흡수합니다. 프로세스 로컬 게이지는 이 프로세스 자신의 메모리를 나타내므로 합산은 의미가 없습니다. 히스토그램은 레플리카의 버킷을 합산한 뒤 분위수를 계산합니다.
| 계열 | 분류 | 집계 규칙 |
|---|---|---|
rate_limit_checks_total | 레플리카별 집행 카운터 | rate()에 sum 적용 |
rate_limit_exceeded_total{limit_type} | 레플리카별 집행 카운터 | rate()에 sum by (limit_type) 적용 |
rate_limit_tokens_remaining{bucket_type, identifier} | 공유 상태 뷰 | 합산 금지, 시리즈 하나를 관측하거나 max by (bucket_type) 사용 |
rate_limit_bucket_capacity{bucket_type} | 프로세스 로컬 게이지 | 합산 금지, max by (bucket_type) 사용 |
rate_limit_whitelisted_requests_total, rate_limit_bypassed_requests_total | 레플리카별 집행 카운터 | 계열마다 rate()에 sum 적용 |
rate_limit_skipped_total{dimension} | 레플리카별 집행 카운터 | rate()에 sum by (dimension) 적용 |
rate_limit_redis_unavailable_total{dimension, action} | 레플리카별 집행 카운터 | rate()에 sum by (action) 적용 |
rate_limit_token_budget_tokens_total{dimension, outcome} | 레플리카별 집행 카운터 | rate()에 sum by (dimension, outcome) 적용 |
rate_limit_token_budget_period_tokens_total{dimension, budget, outcome} | 레플리카별 집행 카운터 | rate()에 sum by (dimension, budget, outcome) 적용 |
rate_limit_token_budget_refusals_total{dimension} | 레플리카별 집행 카운터 | rate()에 sum by (dimension) 적용 |
rate_limit_token_budget_period_refusals_total{dimension, budget} | 레플리카별 집행 카운터 | rate()에 sum by (dimension, budget) 적용 |
rate_limit_inflight_requests{dimension, tenant} | 프로세스 로컬 게이지 | 합산 금지, max by (dimension) 사용 |
rate_limit_inflight_refusals_total{dimension, tenant} | 레플리카별 집행 카운터 | rate()에 sum by (dimension) 적용 |
rate_limit_fair_queue_depth | 프로세스 로컬 게이지 | 합산 금지, max 또는 레플리카 평균 사용 |
rate_limit_fair_queue_tenants | 프로세스 로컬 게이지 | 합산 금지, max 사용 |
rate_limit_fair_queue_wait_seconds{tenant_dimension} | 히스토그램 | 레플리카의 버킷을 합산한 뒤 histogram_quantile 적용 |
rate_limit_fair_queue_grants_total{tenant_dimension} | 레플리카별 집행 카운터 | rate()에 sum by (tenant_dimension) 적용 |
rate_limit_fair_queue_refused_total{tenant_dimension, reason} | 레플리카별 집행 카운터 | rate()에 sum by (tenant_dimension, reason) 적용 |
router_budget_rejections_total{scope, window} | 레플리카별 집행 카운터 | rate()에 sum by (scope, window) 적용 |
router_budget_used{scope, window, unit} | 프로세스 로컬 게이지 | 합산 금지, 시리즈별 관측 또는 max 사용 |
router_budget_storage_unavailable_total{action} | 레플리카별 집행 카운터 | rate()에 sum by (action) 적용 |
버킷 자체가 공유될 때(storage: redis) rate_limit_tokens_remaining은 공유 상태 뷰입니다. 모든 레플리카가 같은 버킷을 관측하므로 시리즈를 합산하면 이중 계산되고, 시리즈 하나 또는 max by (bucket_type)이 배포 전체 수준을 나타냅니다. 인메모리 백엔드는 레플리카마다 버킷을 따로 두므로 각 시리즈는 해당 레플리카의 몫만 나타냅니다.
경보는 레플리카 한 개의 시리즈가 아니라 집계값에 걸어야 합니다. 레플리카가 추가되거나 제거되면 같은 함대 트래픽이 더 많은 시리즈로 나뉘거나 시리즈 하나가 사라지는데 함대 전체 값은 그대로이므로, 단일 시리즈의 고정 임계값은 잘못 울립니다. 카운터는 시리즈를 합산한 뒤 rate()를 적용하고, 히스토그램은 버킷을 합산한 뒤 histogram_quantile을 적용합니다. 레플리카별 분위수를 평균해서는 안 됩니다.
# 카운터: 레플리카 한 개의 시리즈가 아니라 함대 집계값을 사용합니다.
sum by (limit_type) (rate(rate_limit_exceeded_total[5m])) > 1
# 히스토그램: 레플리카의 버킷을 합산한 뒤 분위수를 계산합니다.
histogram_quantile(
0.95,
sum by (le, tenant_dimension) (rate(rate_limit_fair_queue_wait_seconds_bucket[5m]))
) > 5
로깅¶
속도 제한 이벤트는 컨텍스트와 함께 로깅됩니다:
{
"level": "warn",
"msg": "속도 제한 초과",
"client_id": "abc123...",
"endpoint": "/v1/chat/completions",
"limit_type": "burst",
"limit_value": 20,
"window": "5s"
}
우회 메커니즘¶
IP 화이트리스트¶
신뢰할 수 있는 IP 주소나 CIDR 범위를 화이트리스트에 추가합니다:
rate_limiting:
whitelist:
- "192.168.1.0/24" # 내부 네트워크
- "10.0.0.1" # 관리자 서버
- "172.16.0.0/16" # 사내 네트워크
API 키 우회¶
특정 API 키가 모든 속도 제한을 우회하도록 설정할 수 있습니다:
헬스 체크와 메트릭 면제¶
라우터가 포화 상태에서도 프로브에 응답할 수 있도록 생존·준비성 검사 경로는 항상 면제됩니다:
/health/health/ready/healthz
메트릭 엔드포인트 또한 metrics.enabled가 true인 동안 설정된 metrics.path 경로에 대해 면제됩니다. 메트릭이 비활성화된 상태에서는 /metrics도 일반 미매칭 경로와 동일하게 계량됩니다. 면제된 메트릭 경로는 속도 제한기로 스크레이프 폭주를 차단하지 않으며 메트릭 카디널리티에 비례해 직렬화 비용이 커지므로, metrics.auth를 설정하고 네트워크 수준에서도 접근을 제한하십시오.
버킷 용량¶
식별자별 버킷 맵(per_client, per_api_key, per_user, per_team, per_backend)은 각각 최대 100,000개 항목을 유지하며, 비활성 항목은 600초 후 만료됩니다. 만료된 항목을 정리한 뒤에도 맵이 가득 차 있으면, 해당 요청에 대해서는 그 차원의 평가를 건너뛰고 요청을 허용하며 나머지 제한 차원은 그대로 집행합니다.
테이블 포화 시 요청을 거절(fail-closed)하지 않고 통과(fail-open)시키는 이유는 출발지 주소 고갈 공격을 막기 위해서입니다. 예를 들어 단일 IPv6 /64 서브넷만으로도 100,000개의 서로 다른 출발지 주소를 쉽게 생성할 수 있으므로, 테이블이 찼을 때 요청을 거절하면 공격자가 10분 동안 모든 신규 호출자의 접속을 차단할 수 있습니다. 평가를 건너뛴 횟수는 rate_limit_skipped_total{dimension} 메트릭에 기록되며, 분당 최대 1회로 제한된 경고 로그가 출력됩니다.
스토리지 백엔드¶
메모리 스토리지 (기본)¶
인메모리 스토리지는 빠르지만 인스턴스 간에 상태가 공유되지 않습니다:
장점:
- 외부 의존성 없음
- 낮은 지연 시간
- 간단한 설정
단점:
- 라우터 인스턴스 간 공유되지 않음
- 재시작 시 초기화됨
- 단일 인스턴스 배포에만 적합
Redis 스토리지 (분산)¶
Redis 스토리지를 사용하면 여러 라우터 인스턴스에 걸쳐 분산 속도 제한을 적용할 수 있습니다:
rate_limiting:
storage: redis
redis:
url: "redis://localhost:6379"
key_prefix: "continuum:ratelimit:" # 속도 제한 키 접두사 (기본값)
ttl: 3600 # 속도 제한 키 TTL (초)
자격 증명은 URL에 포함할 수 있습니다 (redis://:password@host:6379). rate_limiting.redis.url 필드는 ${VAR} 환경 변수 참조를 확장하지 않으므로, 설정 검증 전에 완성된 URL을 주입하십시오.
장점:
- 모든 라우터 인스턴스 간 공유
- 재시작 후에도 상태 유지
- 정확한 전역 제한
단점:
- Redis 인프라 필요
- 인메모리 대비 약간의 지연 시간 증가
- 추가적인 운영 복잡성
저장소 장애¶
Redis 저장소를 사용하는 모든 제한(전역, API 키별, 클라이언트별, 모델별, 백엔드별)은 같은 장애 정책을 따릅니다.
- Fail-open (기본값): Redis에 연결할 수 없으면 제한을 해제하는 대신 각 복제본의 로컬 인메모리 버킷으로 검사를 집행합니다. 따라서 Redis가 복구될 때까지 복제본 N개로 이루어진 플릿은 설정된 속도의 최대 N배까지 허용합니다.
rate_limit_redis_unavailable_total{action="fallback"}에 집계됩니다. - Fail-closed (
redis.fallback_to_memory: false): 검사를503 service_unavailable과Retry-After: 5로 거절합니다. 쿼터 위반이 아니라rate_limit_redis_unavailable_total{action="refused"}에 집계됩니다. - 검사 시간 상한: 저장소에 연결할 수 없는 Redis 검사는 시간 상한이 있습니다(풀 타임아웃 250ms, 검사당 500ms). 이런 실패가 한 번 발생하면 라우터는 5초 동안 Redis를 건너뛰고 설정된 정책을 즉시 적용하며, 이후 요청 하나만 Redis를 다시 확인합니다. 이 확인이 성공하면 분산 검사를 재개합니다.
핫 리로드 지원¶
속도 제한 설정은 재시작 없이 즉시 반영되는 핫 리로드를 지원합니다:
# 이 설정들은 재시작 없이 즉시 업데이트됩니다
rate_limiting:
enabled: true # ✅ 즉시: 속도 제한 활성화/비활성화
limits:
per_client:
requests_per_second: 10 # ✅ 즉시: 새 제한이 즉시 적용
burst_capacity: 20 # ✅ 즉시: 버스트 설정이 즉시 업데이트
max_in_flight_requests: 8 # ✅ 즉시: 새 상한이 다음 수락부터 적용
per_backend:
requests_per_second: 100 # ✅ 즉시: 백엔드 제한이 즉시 업데이트
모범 사례¶
1. 보수적으로 시작¶
처음에는 보수적인 한도로 시작하고, 실제 트래픽 추이를 보며 점진적으로 완화하십시오:
2. 모니터링 및 조정¶
메트릭을 사용하여 실제 트래픽 패턴을 파악하십시오:
rate_limit_exceeded_total메트릭 추적- 정상 트래픽과 비정상 남용 트래픽 구분
- 관측 데이터에 기반하여 한도 조정
3. 다른 계층 사용¶
키별 오버라이드(분당 요청 수)를 사용하여 사용자 등급별로 차등 속도 제한을 구성하십시오:
api_keys:
api_keys:
- key: "free-tier-key"
rate_limit: 60
- key: "pro-tier-key"
rate_limit: 600
- key: "enterprise-tier-key"
rate_limit: 6000
4. 고비용 모델 보호¶
비용이 많이 드는 모델에는 per_model 제한으로 더 엄격한 한도를 적용하십시오:
모델 이름은 클라이언트가 요청에 적는 이름 그대로 지정하십시오. 버킷은 요청 경로나 JSON 본문에 담긴 클라이언트 대면 이름을 키로 사용하므로, 별칭의 대상 모델이나 실험 변형 모델에 설정한 항목은 다른 클라이언트 대면 이름으로 유입되는 트래픽을 제한하지 않습니다. 자세한 내용은 버킷을 고르는 이름을 참고하십시오.
5. 프로덕션에서는 Redis 사용¶
다중 인스턴스 배포 환경에서는 Redis를 사용하십시오:
문제 해결¶
일반적인 문제¶
속도 제한이 적용되지 않음¶
증상: 클라이언트가 설정된 제한을 초과할 수 있음
해결책:
- 클라이언트가 화이트리스트에 있는지 확인
rate_limiting.enabled가true인지 확인- 속도 제한 초기화 로그 확인
- API 키 형식이 올바른지 확인
너무 많은 오탐지¶
증상: 정상 트래픽이 속도 제한에 걸림
해결책:
- 버스트 트래픽을 수용하도록
burst_capacity증가 - 클라이언트 식별 설정 검토 (단일 프록시 IP로 여러 클라이언트가 묶여 있는지 확인)
- API 키별 제한(
per_api_key) 사용 고려 - 정상 내부 IP를 화이트리스트에 추가
Redis 연결 문제¶
증상: Redis 스토리지에서 속도 제한이 작동하지 않음
해결책:
- Redis 네트워크 연결 확인
- Redis 인증 정보 확인
- 연결 풀 설정 검토
- Redis 성능 모니터링
디버그 로깅¶
디버그 로깅을 활성화하면 속도 제한 판정 과정을 상세히 확인할 수 있습니다:
관련 문서¶
- 속도 제한 아키텍처 - 구현 세부 사항 및 설계 결정
- 설정 가이드 - 전체 설정 참조
- 관리자 API - 런타임 설정 관리
- 메트릭 - 모니터링 및 관측성
- 오류 처리 - 오류 코드 및 재시도 전략