← 에디토리얼

운영 · 2026.08.22 · 8분

LLM 요금이 갑자기 뛰는 다섯 가지 이유

시범 운영 때 월 3만 원이던 게 오픈하고 두 달 만에 300만 원이 됩니다. 사용자가 늘어서가 아니라 요청 하나에 실어 보내는 양이 늘어서입니다.

LLM 기능은 개발비보다 운영비에서 놀라는 일이 많습니다. 시범 운영 때 월 3만 원이던 게 오픈하고 두 달 만에 300만 원이 되기도 합니다.

사용자가 100배 늘어서가 아닙니다. 요청 하나에 실어 보내는 양이 늘어서입니다.

먼저 가격 구조부터

뭘 줄일지 정하려면 뭐가 얼마인지 알아야 합니다. Claude 기준 100만 토큰당 값입니다.

모델 입력 출력 출력이 입력의
Opus 5 $5 $25 5배
Sonnet 5 $2 $10 5배
Haiku 4.5 $1 $5 5배

여기서 두 가지가 보입니다.

출력이 입력보다 5배 비쌉니다. 그런데 다들 입력만 신경 씁니다.

모델끼리 5배 차이가 납니다. 같은 일을 Haiku로 하면 Opus의 5분의 1입니다.

여기에 캐시가 붙습니다. 캐시에 쓸 때는 1.25배쯤, 캐시에서 읽을 때는 0.1배쯤입니다. 같은 내용을 반복해서 보낸다면 캐시를 켜는 것만으로 그 부분이 90% 싸집니다.

1. 같은 걸 매번 다시 보냅니다

제일 흔합니다. API는 상태를 기억하지 않아서, 대화형 기능은 요청할 때마다 지금까지의 대화를 통째로 다시 보냅니다. 열 번째 질문에서는 앞의 아홉 번을 다시 보내는 셈입니다.

여기에 안내 문서나 규정을 매번 붙이면 이렇게 불어납니다.

1번째 요청   지시문 2,000 + 문서 6,000 + 대화 200   =  8,200 토큰
5번째 요청   지시문 2,000 + 문서 6,000 + 대화 2,400 = 10,400 토큰
10번째 요청  지시문 2,000 + 문서 6,000 + 대화 5,600 = 13,600 토큰

사용자는 "네"라고 한 글자 보냈는데 실제로는 13,600 토큰이 나갑니다. 10턴이면 누적 10만 토큰, Opus 기준 입력만 $0.5입니다. 하루 1,000명이면 하루 $500이고요.

캐싱이 어떻게 도는지

프롬프트 캐싱은 앞에서부터 같은 만큼만 맞습니다. 요청은 이 순서로 조립됩니다.

tools  →  system  →  messages

앞에서부터 바이트가 똑같은 구간까지가 캐시 대상입니다. 중간에 한 글자만 바뀌어도 그 뒤는 전부 날아갑니다. 그래서 어디에 뭘 두느냐가 곧 비용입니다.

const response = await client.messages.create({
  model: "claude-opus-5",
  max_tokens: 16000,
  system: [
    {
      type: "text",
      text: STABLE_INSTRUCTIONS + POLICY_DOCUMENT, // 안 바뀌는 것
      cache_control: { type: "ephemeral" },        // 여기까지 캐시
    },
  ],
  messages: [...history, { role: "user", content: question }],
});

안 바뀌는 걸 앞에, 매번 바뀌는 걸 뒤에 둡니다. 캐시 구간은 최대 4개까지 잡을 수 있고 기본 5분 유지됩니다. 1시간짜리도 있습니다.

캐시가 안 먹으면 대개 셋 중 하나

켰는데도 값이 안 줄면 어딘가에서 조용히 깨지고 있는 겁니다. 확인은 응답에서 바로 됩니다.

console.log(response.usage.cache_read_input_tokens);     // 캐시에서 읽은 양
console.log(response.usage.cache_creation_input_tokens); // 캐시에 쓴 양
console.log(response.usage.input_tokens);                // 캐시 못 탄 양

같은 프롬프트를 반복해서 부르는데 cache_read_input_tokens가 계속 0이라면 범인은 보통 이렇습니다.

범인 왜 깨지나
지시문에 new Date() 요청마다 바이트가 달라져 전부 무효
JSON 키 순서가 들쭉날쭉 같은 객체라도 순서가 다르면 다른 바이트
요청마다 도구 목록이 다름 tools가 맨 앞이라 뒤가 전부 무효

특히 첫 번째가 많습니다. "오늘은 2026년 8월 22일입니다"를 지시문에 넣으면 그날 하루는 버티지만, 시각까지 넣으면 모든 요청이 캐시를 놓칩니다. 날짜가 필요하면 지시문 말고 마지막 사용자 메시지에 붙이세요.

하나 더. 캐시가 걸리는 최소 길이가 있습니다. 모델에 따라 512~4,096 토큰이고, 그보다 짧으면 켜도 아무 일도 안 일어납니다. 지시문이 300 토큰이면 캐시를 켠 의미가 없습니다.

2. 큰 모델 하나로 다 처리합니다

"제일 좋은 걸 쓰자"로 시작해서 끝까지 그걸로 갑니다. 그런데 요청을 뜯어보면 대부분은 어려운 일이 아닙니다.

  • 문의를 유형별로 나누기
  • 짧은 문장 다듬기
  • 정해진 형식으로 바꾸기

이런 건 작은 모델로 충분하고 값은 5분의 1입니다. 어려운 판단만 큰 모델로 넘기는 구조를 처음부터 잡아두면 나중에 갈아엎지 않아도 됩니다.

// 분류처럼 단순한 건 작은 모델로
const kind = await client.messages.create({
  model: "claude-haiku-4-5",
  max_tokens: 256,                 // 분류는 출력이 짧다
  system: CLASSIFY_PROMPT,
  messages: [{ role: "user", content: inquiry }],
});

// 갈라서, 어려운 것만 큰 모델로
if (needsJudgement(kind)) {
  await client.messages.create({ model: "claude-opus-5", /* ... */ });
}

다만 재보고 정해야 합니다. 큰 모델을 낮은 사고 수준으로 돌리는 쪽이 더 싸고 정확한 경우도 많습니다. 모델을 섞으면 캐시가 모델별로 따로 잡혀서, 캐시로 아끼던 걸 도로 잃기도 합니다.

한 요청이 실패해서 두 번 돌면 싼 모델이 아닙니다. 요청당 값이 아니라 일 하나를 끝내는 데 든 값으로 봐야 합니다.

3. 출력 길이를 안 정해둡니다

출력이 5배 비싼데 길이는 신경을 덜 씁니다.

"자세히 설명해 주세요"가 지시문에 들어 있으면 모델은 성실하게 길게 씁니다. 화면에는 세 줄만 보이는데 2,000 토큰을 만들어내는 일이 흔합니다.

출력 2,000 토큰 × Opus $25/1M = $0.05
하루 10,000건이면 하루 $500, 한 달 $15,000

같은 화면을 300 토큰으로 채울 수 있다면 한 달 $2,250입니다. 하는 일은 똑같은데요.

  • max_tokens를 화면에 실제로 들어가는 분량에 맞춥니다
  • 프롬프트에 길이를 못 박습니다 ("세 문장 이내로")
  • 사고 깊이를 조절하는 설정이 있다면 일상적인 요청은 낮춥니다

4. 실패한 요청을 그대로 다시 보냅니다

타임아웃이 나면 다시 시도합니다. 다시 보낸 것도 과금됩니다. 횟수 제한이 없으면 장애가 났을 때 요청이 쌓이면서 값도 같이 뜁니다.

SDK는 기본으로 두 번까지 다시 시도하고 타임아웃도 그 대상입니다. 타임아웃 10분에 재시도 2회면 한 요청이 최대 30분을 붙잡고 그동안 세 번 과금될 수 있습니다.

const client = new Anthropic({
  maxRetries: 1,      // 기본 2. 사용자가 기다리는 화면이면 1이면 된다
  timeout: 60_000,    // TypeScript는 밀리초, Python·Ruby는 초 단위다
});

여기에 사용자당·시간당 호출 상한을 겁니다. 비용을 막는 동시에 악용도 막습니다. 공개된 화면에 LLM을 붙이면 자동화된 호출이 반드시 들어옵니다.

5. 얼마 나가는지 안 보고 있습니다

이게 진짜 원인입니다. 대부분 청구서가 와야 압니다.

응답의 usage에 필요한 숫자가 다 들어 있습니다. 기능 이름과 함께 남기는 데 열 줄이면 됩니다.

await logUsage({
  feature: "inquiry-classify",   // 기능별로 나눠야 범인을 찾는다
  model: response.model,
  input: response.usage.input_tokens,
  cacheRead: response.usage.cache_read_input_tokens,
  cacheWrite: response.usage.cache_creation_input_tokens,
  output: response.usage.output_tokens,
});

기능별로 나눠두면 "어제부터 갑자기 늘었는데 뭐 때문이냐"에 바로 답할 수 있습니다. 나중에 붙이려면 로그 구조부터 다시 짜야 합니다.

보내기 전에 미리 세볼 수도 있습니다. 토큰은 어림잡지 말고 전용 엔드포인트로 세야 정확합니다.

const { input_tokens } = await client.messages.countTokens({
  model: "claude-opus-5",
  system,
  messages,
});

급하지 않은 일은 배치로

당장 답이 필요 없는 작업 — 야간 일괄 분류, 문서 요약, 데이터 정리 — 은 배치로 돌리면 값이 절반입니다. 몇 분에서 몇 시간이 걸리지만 아무도 기다리지 않는 일이라면 그냥 반값입니다.

코드도 크게 바뀌지 않습니다. 요청을 모아 넣고 결과를 custom_id로 받아오면 됩니다. 결과가 넣은 순서대로 오지 않으니 반드시 custom_id로 맞춰야 합니다.

정리

비용은 사용자 수가 아니라 요청 하나에 얼마나 실어 보내느냐로 정해집니다. 순서대로 하면 됩니다.

  1. 캐싱 — 거의 공짜인데 효과가 제일 큽니다. 안 먹으면 cache_read_input_tokens부터 보세요
  2. 입력 정리 — 오래된 대화는 요약하고 길이에 상한을 겁니다
  3. 출력 상한 — 5배 비싼 쪽입니다
  4. 재시도 제한 — 장애가 곧 청구서가 되지 않게
  5. 사용량 기록 — 없으면 나머지 넷을 판단할 수 없습니다

모델 교체는 그다음입니다. 순서를 뒤집어 모델부터 바꾸면 품질은 잃고 값은 생각만큼 안 줄어드는 경우가 많습니다.

관련 프로젝트

다음 글챗봇을 붙였는데 문의가 안 줄어드는 이유