프롬프트 캐시는 "같은 시스템 프롬프트를 매번 다시 처리하지 말고 저장해 두고 읽자"는 기능이다. 쓸 때 웃돈을 내고 읽을 때 크게 깎아 주므로, 몇 번 읽어야 본전인가라는 산수가 생긴다. 캐시 비용과 캐시 없는 비용이 같아지는 그 읽기 횟수가 손익분기다. 이 글은 그 산수를 닫힌 식으로 풀고, 벤더 문서가 산문으로 적어 둔 답과 맞는지 대조한다.
캐시 계층을 어떻게 설계하는가는 LLMOps 캐시 전략이 맡는다. 여기서는 과금 규칙의 산수만 다룬다.
결론부터 적으면, 식은 문서와 정확히 맞았다. 그런데 식이 답하지 못하는 축이 하나 더 있었고 그쪽이 훨씬 위험했다.
손익분기 식
비용 식
기본 입력 단가를 1로 두고 두 배수를 정의한다. 는 캐시에 쓸 때의 배수, 는 캐시에서 읽을 때의 배수다. 프롬프트를 한 번 쓰고 번 읽는다면
캐시 없는 쪽이 인 것은 최초 요청 한 번과 이후 번을 전부 정가로 내기 때문이다.
임계 읽기 횟수
캐시가 이기는 조건은
배수만 넣으면 임계 읽기 횟수가 나온다. 단가가 얼마인지는 들어가지 않는다 — 분자와 분모에서 같이 약분되기 때문이다. Opus든 Haiku든 손익분기 횟수는 같다.
재현
배수 출처
Anthropic 값은 벤더 문서에서 직접 받았다. 문서는 배수를 표로 못박아 둔다 — 5분 캐시 쓰기 1.25x, 1시간 캐시 쓰기 2x, 캐시 읽기 0.1x. 모델 표의 단가도 이 배수를 그대로 따른다(Opus 5는 입력 $5, 5분 쓰기 $6.25, 1시간 쓰기 $10, 읽기 $0.50).
OpenAI와 Google 가격 문서는 우리 실행 환경의 이그레스 정책에 막혀 받을 수 없다.
대신 pip install litellm이 벤더 셋의 단가표를 통째로 들고 온다 — 패키지 안의
model_prices_and_context_window_backup.json이 1.75MB에 모델 3,040개다.
그런데 이건 벤더가 쓴 문서가 아니라 커뮤니티가 옮겨 적은 2차 출처다. 그래서 그대로 싣지 않고, Anthropic이라는 겹치는 칸으로 이 JSON을 먼저 검증한다. 문서에서 받은 배수 셋이 JSON의 anthropic 행에서 지켜지는지 전수 대조하는 것이 이 글의 첫 번째 측정 항목이다.
스크립트
pip install litellm
python cache_breakeven.py
import json, os, math, litellm
D = json.load(open(os.path.join(os.path.dirname(litellm.__file__),
"model_prices_and_context_window_backup.json")))
MULT = {"cache_read_input_token_cost": (0.1, "읽기"),
"cache_creation_input_token_cost": (1.25, "5분쓰기"),
"cache_creation_input_token_cost_above_1hr": (2.0, "1시간쓰기")}
DOC_MIN = {"claude-opus-5": 512, "claude-fable-5": 512, "claude-mythos-5": 512,
"claude-mythos-preview": 2048, "claude-opus-4-7": 2048, "claude-opus-4-8": 1024,
"claude-opus-4-6": 4096, "claude-opus-4-5": 4096, "claude-sonnet-5": 1024,
"claude-sonnet-4-6": 1024, "claude-sonnet-4-5": 1024, "claude-haiku-4-5": 4096}
print("# 1. 문서가 못박은 배수를 2차 출처가 지키는가 — anthropic 행 전수 대조")
ok = bad = 0
for n, v in sorted(D.items()):
if not isinstance(v, dict) or v.get("litellm_provider") != "anthropic":
continue
base = v.get("input_cost_per_token")
if not base or "cache_read_input_token_cost" not in v:
continue
msg = []
for k, (m, ko) in MULT.items():
got = v.get(k)
if got is None:
msg.append(f"{ko} 필드 없음")
elif abs(got / base - m) > 1e-9:
msg.append(f"{ko} {got / base:.4g}x (문서 {m}x)")
if n in DOC_MIN and v.get("prompt_cache_min_tokens") != DOC_MIN[n]:
msg.append(f"최소토큰 {v.get('prompt_cache_min_tokens')} (문서 {DOC_MIN[n]})")
if msg:
bad += 1
print(f" {n:26s} " + ", ".join(msg))
else:
ok += 1
print(f" → 대조 {ok + bad}행: 일치 {ok}, 어긋남 {bad} ({100 * ok / (ok + bad):.1f}% 일치)\n")
print("# 2. 손익분기 읽기 횟수: w + N·r < N + 1 → N > (w-1)/(1-r)")
for lab, w, r in [("Anthropic 5분", 1.25, 0.1), ("Anthropic 1시간", 2.0, 0.1),
("OpenAI 자동캐시", 1.0, 0.25), ("Gemini 암묵캐시", 1.0, 0.25)]:
t = (w - 1) / (1 - r)
print(f" {lab:16s} w={w:<5} r={r:<5} 임계 N > {t:.4f} → 읽기 {math.floor(t) + 1}회부터 이득")
def total(reqs, gap, ttl, w, r):
cost, last = 0.0, None
for i in range(reqs):
t = i * gap
cost += w if (last is None or t - last > ttl) else r
last = t # 읽기도 TTL을 갱신한다
return cost
print("\n# 3. 요청 20회의 총 입력 비용 (기본 입력 단가를 1로 둔 상대값)")
print(f" {'요청 간격':>8} | {'캐시 없음':>9} | {'5분 TTL':>8} | {'1시간 TTL':>9}")
for gap, lab in [(60, "1분"), (600, "10분"), (1800, "30분"), (7200, "2시간")]:
a, b = total(20, gap, 300, 1.25, 0.1), total(20, gap, 3600, 2.0, 0.1)
print(f" {lab:>8} | {20.0:9.2f} | {a:8.2f} | {b:9.2f}")
출력
# 1. 문서가 못박은 배수를 2차 출처가 지키는가 — anthropic 행 전수 대조
claude-3-haiku-20240307 읽기 0.12x (문서 0.1x), 5분쓰기 1.2x (문서 1.25x), 1시간쓰기 24x (문서 2.0x)
claude-3-opus-20240229 1시간쓰기 0.4x (문서 2.0x)
claude-4-opus-20250514 1시간쓰기 필드 없음
claude-4-sonnet-20250514 1시간쓰기 필드 없음
claude-mythos-preview 최소토큰 512 (문서 2048)
→ 대조 26행: 일치 21, 어긋남 5 (80.8% 일치)
# 2. 손익분기 읽기 횟수: w + N·r < N + 1 → N > (w-1)/(1-r)
Anthropic 5분 w=1.25 r=0.1 임계 N > 0.2778 → 읽기 1회부터 이득
Anthropic 1시간 w=2.0 r=0.1 임계 N > 1.1111 → 읽기 2회부터 이득
OpenAI 자동캐시 w=1.0 r=0.25 임계 N > 0.0000 → 읽기 1회부터 이득
Gemini 암묵캐시 w=1.0 r=0.25 임계 N > 0.0000 → 읽기 1회부터 이득
# 3. 요청 20회의 총 입력 비용 (기본 입력 단가를 1로 둔 상대값)
요청 간격 | 캐시 없음 | 5분 TTL | 1시간 TTL
1분 | 20.00 | 3.15 | 3.90
10분 | 20.00 | 25.00 | 3.90
30분 | 20.00 | 25.00 | 3.90
2시간 | 20.00 | 25.00 | 40.00
문서 대조
Anthropic 배수
문서는 배수 표 아래에 이렇게 적는다 — "캐시는 5분 지속(1.25x 쓰기)에서는 캐시 읽기 1회 후에, 1시간 지속(2x 쓰기)에서는 2회 후에 이득이 된다."
식이 낸 값은 과 이고, 정수로 올리면 1회와 2회다. 문서의 산문과 정확히 같다. 문서가 어떤 계산으로 그 문장을 썼는지가 이걸로 닫힌다.
읽기 배수가 0.1이라 분모 이 0.9로 거의 1이다. 그래서 임계값이 사실상 이고, 쓰기 웃돈이 곧 필요한 읽기 횟수다. 5분 캐시의 웃돈 0.25는 한 번만 읽어도 회수되고, 1시간 캐시의 웃돈 1.0은 두 번 읽어야 한다.
OpenAI와 Gemini
OpenAI와 Gemini는 모양이 다르다. JSON에 캐시 쓰기 단가 필드 자체가 없다 —
93개 openai 행과 32개 gemini 행 중 cache_creation_input_token_cost를 가진
것이 하나도 없다. 쓰기가 공짜라 이고 임계값이 0이 된다. 읽기 배수는
0.25x(일부 파인튜닝 모델은 0.5x)다. 즉 이 둘에서는 손익분기를 따질 일이
없다. 캐시가 맞으면 이득이고 안 맞으면 본전이다. 대신 무엇을 캐시할지 고를
권한도 없다.
litellm 행 대조
전수 대조 결과는 26행 중 21행 일치, 5행 어긋남(80.8%)이다. 어긋난 다섯을 보면 성격이 갈린다.
claude-3-opus-20240229의 1시간 쓰기가 기본 입력가의 0.4x다. 캐시 쓰기가 정가보다 싸다는 뜻이라 문서를 안 봐도 틀린 줄 안다. 값(6e-06)이 Sonnet 계열의 1시간 쓰기와 같은 것으로 보아 옮겨 적다 섞인 자리다.claude-3-haiku-20240307은 세 배수가 전부 어긋난다. 1시간 쓰기가 기본가의 24배다.claude-4-opus-20250514·claude-4-sonnet-20250514는 1시간 쓰기 필드가 아예 없다. 같은 모델의 다른 별칭인claude-opus-4-20250514에는 값이 있다. 별칭마다 데이터가 다르다는 것이 이 JSON의 구조적 약점이다.claude-mythos-preview의 최소 토큰이 512인데 문서는 2,048이라고 적는다. 넷과 달리 이건 산술로는 못 잡는다 — 문서를 봐야만 걸린다.
어긋난 것이 전부 은퇴했거나 별칭인 행이고 현행 주력 모델 행은 다 맞았다는 점이 실무적으로는 다행이다. 하지만 80.8%라는 숫자가 이 글이 OpenAI·Gemini 칸에 붙이는 오차 표시다. 그쪽은 대조할 문서가 없어 검증하지 못했고, 같은 비율로 틀렸다면 다섯 행에 하나꼴이다.
TTL 절벽
캐시 갱신
위 식에는 시간이 안 들어간다. 그런데 캐시에는 TTL, 곧 저장된 캐시가 만료되기까지의 유지 시간이 있고, 문서의 캐시 설명 쪽에 식을 완전히 바꾸는 문장이 하나 더 있다.
캐시된 내용이 사용될 때마다 추가 비용 없이 캐시가 갱신된다.
읽기가 TTL 타이머를 되돌린다는 뜻이다. 그러면 요청 간격 가 TTL보다 짧은 한 쓰기는 영원히 한 번뿐이다. 반대로 가 TTL보다 길면 캐시는 매번 만료돼 있고, 모든 요청이 쓰기가 된다. 그때 비용은
읽기가 한 번도 안 일어나므로 손익분기 식이 적용될 자리가 없고, 요청마다 만큼 순손실이다. 5분 캐시면 +25%, 1시간 캐시면 +100%다.
요청 20회 비용
출력 3번 표가 이 절벽이다. 요청 20회를 같은 프롬프트로 보낼 때 —
| 요청 간격 | 캐시 없음 | 5분 TTL | 1시간 TTL |
|---|---|---|---|
| 1분 | 20.00 | 3.15 | 3.90 |
| 10분 | 20.00 | 25.00 | 3.90 |
| 30분 | 20.00 | 25.00 | 3.90 |
| 2시간 | 20.00 | 25.00 | 40.00 |
5분 TTL은 간격이 1분에서 10분으로 늘어나는 사이에 3.15에서 25.00으로 뛴다. 7.9배이고, 캐시를 아예 안 쓴 20.00보다도 비싸다. 곡선이 완만하게 나빠지는 것이 아니라 TTL 경계에서 수직으로 선다 — 간격이 TTL보다 1초라도 길면 적중률이 100%에서 0%로 떨어지기 때문이다.
꺾이는 지점
요청 간격별 선택
손익분기를 결정하는 변수는 읽기 횟수가 아니라 요청 간격이고, 경계는 정확히 TTL이다. 트래픽이 뜸해지는 구간의 요청 간격을 재서 규칙을 고른다.
| 요청 간격 | 고를 것 | 근거 |
|---|---|---|
| 5분 미만 | 5분 TTL | 쓰기 1회 + 읽기 N회. 웃돈 0.25를 첫 읽기에 회수 |
| 5분 ~ 1시간 | 1시간 TTL | 5분 TTL은 +25% 순손실. 1시간 TTL은 읽기 2회부터 이득 |
| 1시간 초과 | 캐시 끄기 | 5분 +25%, 1시간 +100%. 둘 다 안 쓰느니만 못하다 |
평균 간격이 아니라 꼬리를 봐야 한다. 평균 2분이어도 야간에 20분씩 벌어지는 서비스라면 그 구간의 요청은 전부 25% 웃돈짜리 쓰기다.
최소 캐시 길이
그리고 그 위에 하드 게이트가 하나 더 있다. 캐시가 걸리는 프롬프트의 최소 토큰 수, 곧 최소 캐시 길이다. 문서의 표는
Opus 5가 512토큰, Sonnet 5가 1,024토큰, Opus 4.6과 Haiku 4.5가 4,096토큰이라고
적는다. 그보다 짧은 프롬프트는 cache_control을 붙여도 캐시되지 않는데,
에러가 나지 않는다. 문서가 직접 적어 둔 확인 방법은 응답의
cache_creation_input_tokens와 cache_read_input_tokens를 보는 것이다 —
둘 다 0이면 캐시가 안 걸린 것이다. 모델마다 문턱이 8배까지 차이 나므로
모델을 바꾸면 어제까지 캐시되던 프롬프트가 조용히 안 캐시된다.
실험의 범위
한계
- 이 글은 청구서를 보고 쓴 것이 아니다. 벤더 문서의 규칙과 그 규칙으로 푼 산수이지, 실제 과금 명세와 대조한 것이 아니다. API 키가 필요한 검증이라 하지 못했다.
- 3번 표는 시뮬레이션이지 측정이 아니다. 요청 간격이 일정하다고 가정했고, 캐시 적중을 TTL 안이면 100%로 뒀다. 실제로는 프롬프트 접두사가 조금만 달라도 적중이 깨지고, 여러 인스턴스가 각자 캐시를 쓰면 쓰기가 그만큼 늘어난다.
- OpenAI·Gemini 칸은 전부 2차 출처다. 위 80.8%가 그 칸에 붙는 유일한 신뢰도 정보다. Gemini의 명시적 컨텍스트 캐시에 붙는 시간당 저장 요금은 이 JSON에 필드가 없어 표에서 뺐다 — 그 요금이 있으면 손익분기 식에 시간 항이 하나 더 붙는다.
- 배타 조건은 다루지 않았다. 배치 API·Fast mode·데이터 레지던시 배수가 캐시 배수와 어떤 순서로 곱해지는지는 다음 글의 자리다.
측정 환경
| 항목 | 값 |
|---|---|
| OS | Ubuntu 24.04.4 LTS · Linux 6.18.44 x86_64 |
| CPU | Intel Xeon @ 2.80GHz · 4코어 |
| Python | 3.11.15 |
| 패키지 | litellm==1.98.0 (model_prices_and_context_window_backup.json 1,747,806바이트 · 모델 3,040개) |
| 1차 출처 | platform.claude.com/docs/en/about-claude/pricing · .../build-with-claude/prompt-caching |
| 실행 시간 | 3초 미만 (네트워크 접근 없음) |
| 측정일 | 2026-08-28 |
읽어주셔서 감사합니다. 😊

