스무 개의 에이전트가 동시에 시작합니다. 각 에이전트는 예산을 확인하고, $5.00의 상한선 대비 $4.10이 지출되었음을 확인하며, 약 $0.15가 드는 요청을 보냅니다. 스무 번의 모든 확인이 통과됩니다. 이들이 합쳐서 $3.00를 추가하여, 최종적으로 $5.00 상한선에서 $7.10에 도달하게 됩니다.
아무것도 충돌하지 않았고 오류도 발생하지 않았습니다. 감시자는 요청이 나갈 무렵 이미 오래된 숫자를 읽었고, 모든 에이전트가 그 동일한 숫자를 읽었습니다.
대부분의 자체 제작 LLM 예산 코드는 이 버그를 가지고 있습니다. 통제팀이 일반적으로 의존하는 방법들도 이를 잡아내지 못합니다. 아래에서는 각 제어가 어떻게 실패하는지 설명하고, 지출을 막는 코드를 제공합니다.
가장 잘 측정된 팀들이 가장 많이 초과한다
2026년 2월, Sapio Research가 DoiT를 위해 대형 미국 및 영국 기업의 재무 리더 500명을 대상으로 설문조사를 실시했습니다. 응답자의 79%가 지난 12개월 동안 AI 비용 초과를 경험했다고 답했습니다. 흥미로운 결과는 최상위 그룹에서 나타났습니다. FinOps를 "매우 성숙하거나 선도적"이라고 평가한 조직 중 89%가 초과 지출했으며, 평균 초과 지출액은 30.9%였습니다. 이는 연구에서 가장 심각한 수치입니다.
설문조사는 이유를 밝히지 않습니다. 제 추측으로는 좋은 대시보드가 초과 지출을 더 빨리 발견하게 하지만, 일단 발견할 때는 이미 돈이 쓰여버렸기 때문일 것입니다. 이들은 소규모 개발자가 아니라 대기업들입니다. 하지만 그들은 인디 개발자가 처음 설정하는 것과 동일한 통제 장치, 즉 대시보드와 알림에 의존합니다.
출처: DoiT, "AI spending survey".
네 가지 제어 장치와 각각이 놓치는 것
| 제어 장치 | 작동 방식 | 막지 못하는 것 |
|---|
| 예산 알림 (Budget alert) | 임계값에서 알림을 전송합니다. | 모든 것을요. 누군가 메시지를 읽을 때까지 지출은 계속됩니다. |
| ... | | |
제공업체 문서(provider docs)는 첫 번째 행에 명확히 설명하고 있습니다. Google의 청구서 문서(billing docs)에 따르면, 알림 전용 예산은 사용량이나 지출을 "자동으로 제한하지 않습니다(doesn't automatically cap)". Google의 실제 지출 한도는 프로젝트별로 구성되며, 해당 기능을 지원하는 서비스에만 적용되고, 수동으로 제한을 해제할 때까지 해당 서비스를 일시 중지 상태로 유지합니다. OpenAI의 지출 한도 가이드(spend-limit guide)는 "지출 알림은 제한을 강제하지 않는다(Spend alerts do not enforce a cap)"고 명시하며, 알림과 하드 리밋(hard limits)을 별개의 제어 장치로 취급합니다.
두 번째 행이 실제 한도입니다. OpenAI는 한도에 도달하면 organization_spend_limit_exceeded 오류를 발생시켜 요청을 실패시킵니다. 하지만 제공업체 수준의 한도는 계정 또는 워크스페이스(workspace) 레벨에서 작동하며 강제하는 데 시간이 걸립니다. 이들은 고객이 존재한다는 사실을 알지 못합니다. 만약 SaaS를 운영한다면, 사용자 한 명의 통제 불능 루프가 조직 전체 한도를 소진시키고 다른 모든 사용자를 차단할 수 있습니다.
출처: Google Cloud 예산, OpenAI 지출 한도.
순진한 가드(The naive guard)
spent = 0.0
CAP = 5.00
...
이것은 네트워크 왕복 시간(network round trip)을 사이에 두고 확인 후 실행(check-then-act)하는 방식입니다. 이 왕복 시간이 발생하는 동안 도착하는 모든 스레드는 이전의 spent 값을 보게 됩니다. 동시성(concurrency) 하에서는 한도가 아무런 효과가 없으며, 병렬 에이전트를 많이 실행할수록 초과 지출액은 더 커집니다.
해결책: 먼저 예약하고 나중에 정산하기 (Fix: reserve first, settle after)
호출을 하기 전에 최악의 경우 비용(worst-case cost)을 계산합니다. 즉, 예상 입력 토큰에 출력의 max_tokens를 더하는 것입니다. 이 금액을 원자적(atomically)으로 예약합니다. 만약 예산 초과라면 오류를 발생시키고, 제공업체는 요청 자체를 받지 못하게 합니다. 호출이 완료된 후에는 예약을 해제하고 실제 비용을 기록합니다.
코드는 부동 소수점(float) 대신 정수 마이크로 달러(integer micro-dollars)를 계산합니다. 부동 소수점을 사용할 경우, 4.10 + 0.15 * 6이 정확히 5.0보다 약간 높게 나와서 실제로는 들어갈 수 있는 호출을 막을 수도 있습니다.
import math
import threading
...
여기서 두 가지 세부 사항이 중요합니다.
첫째, max_tokens는 필수 인자입니다. 출력 제한이 없으면 비용에 상한선이 없으므로 예약할 것이 없습니다. max_completion_tokens를 사용하는 OpenAI 모델의 경우 대신 이 값을 전달하세요. 추론 토큰(Reasoning tokens)도 여기에 포함되므로 경계는 여전히 유지됩니다.
둘째, 실패하는 호출은 최악의 경우로 청구됩니다. 이는 의도적으로 보수적인 방식입니다. 시간 초과된 요청이라도 비용이 부과될 수 있습니다. 만약 더 관대하게 처리하고 싶다면, 알려진 4xx 오류는 0으로 처리하세요.
Anthropic SDK는 이미 max_tokens를 요구하므로, 변경되는 것은 내부 호출(inner call)과 사용량 필드 이름뿐입니다:
resp = self.client.messages.create(
model=model, messages=messages, max_tokens=max_tokens, **kw
)
...
여기서 20개 에이전트 시나리오를 다시 보여드리며, 가짜 $0.15 호출을 사용했습니다:
from concurrent.futures import ThreadPoolExecutor
budget = Budget(cap_usd=5.00)
...
남은 $0.90에 6번의 호출이 들어갑니다. 나머지 14개는 네트워크에 도달하기 전에 차단됩니다. 실행 결과는 $7.10 대신 $5.00에서 종료됩니다.
여러 프로세스 처리: 잠금(lock)을 Redis로 이동
threading.Lock은 단일 프로세스만 보호합니다. 여러 워커, 파드 또는 큐 소비자 플릿을 사용하는 경우, 확인과 증가를 하나의 Lua 스크립트에서 수행하여 동시에 일어나게 하세요:
-- KEYS[1] = 예산 키(budget key), ARGV[1] = 상한액(cap), ARGV[2] = 금액(amount) (마이크로 달러)
local used = tonumber(redis.call('GET', KEYS[1]) or '0')
if used + tonumber(ARGV[2]) > tonumber(ARGV[1]) then
...
r.eval(LUA, 1, key, cap, worst)로 호출하세요. 정산하려면 INCRBY key (actual - worst)를 실행하는데, 이는 보통 음수입니다. budget:{user_id}:2026-10와 같이 테넌트 및 기간별로 하나의 키를 사용하세요. 이렇게 하면 제공업체가 제공할 수 없는 고객당 상한액을 얻게 됩니다.
이 방식을 유지하고 싶지 않다면
저는 이 패턴을 baar-core라는 오픈 소스 Python 라이브러리로 패키징했습니다. 이는 원자적 예약(atomic reservation)을 수행하고, 제공업체에 연락하기 전에 402 오류를 반환합니다. pip install baar-core로 설치할 수 있습니다. 이 라이브러리는 비용 통제 코드는 사용자가 자신의 청구서에 신뢰하기 전에 읽을 수 있어야 하므로 무료입니다.
제품 전반에 걸쳐 사용자별 예산이 필요한 팀들을 위해 noburn.dev가 이를 기반으로 구축되었습니다. 이 역시 사전 검증(pre-flight enforcement)을 수행하여, 사용자가 예산을 초과하는 순간 해당 사용자의 API 호출을 실행되기 전에 차단합니다.
라이브러리 자체의 중요성보다도 '어디에' 그 검사가 위치하느냐가 더 중요합니다. 알림은 이미 초과된 후에 보고됩니다. 요청이 나가지 전에 이루어진 예약이야말로 그것을 막는 것입니다.
현재 사용하고 계신 스택에서 LLM 호출을 보내기 전에 거부하는 것이 있습니까, 아니면 모든 것이 그 후에 보고합니까?
원문은 https://www.robatdasorvi.com/chapters/building/why-i-built-hard-spending-caps-for-llm-apis에 게시되었습니다.