나의 "프롬프트 캐싱 (prompt caching)" 변경이 비용을 높였다 — 내가 무엇을 잘못했는지에 대하여
요약
프롬프트 캐싱 적용 시 비용이 오히려 증가할 수 있는 원인과 주의사항을 다룹니다. 캐시 쓰기 시 발생하는 프리미엄 요금과 캐시 히트가 발생하지 않는 구조적 문제(가변적인 접두사)를 분석합니다.
핵심 포인트
- 캐시 쓰기(Cache write) 시 입력 요율의 약 1.25배 프리미엄 요금이 부과됨
- 캐시 히트(Cache hit)가 발생해야만 90%의 비용 절감 효과를 얻을 수 있음
- 타임스탬프와 같이 매번 변하는 데이터가 접두사에 포함되면 캐시 효율이 급감함
- 캐시 엔트리를 작성만 하고 재사용하지 않으면 오히려 비용이 상승함
하루 종일 실행하는 에이전트에 프롬프트 캐싱 (prompt caching)을 적용하고 다음 인보이스(invoice)를 확인했더니, 금액이 올라갔습니다. 단순한 반올림 오차가 아니라, 유의미하게 올라갔습니다. 이것은 왜 그런 일이 발생했는지에 대한 기록입니다. 왜냐하면 제가 실수한 부분은 "그냥 캐싱을 추가하세요"라고 말하는 거의 모든 게시물이 생략하는 내용이기 때문입니다.
캐싱에 대한 한 문장 요약
당신은 매 호출마다 동일한 긴 시스템 프롬프트 (system prompt) (여기에 도구 스키마 (tool schemas)와 붙여넣은 문서가 추가됨)를 보냅니다. 캐싱 (caching)이 없다면, 모델이 매번 동일한 블록을 다시 읽도록 하기 위해 전체 입력 가격을 지불해야 합니다. 캐싱 (caching)은 프롬프트의 **접두사 (prefix)**를 저장하며, 다음 호출 시 전체 가격 대신 아주 적은 비용으로 이를 다시 읽어옵니다.
공짜처럼 들립니다. 하지만 공짜가 아닙니다. 그것이 함정입니다.
나를 물린 부분: 쓰기 비용이 더 많이 든다
캐시 엔트리 (cache entry)에는 두 가지 가격이 있는데, 저는 그중 하나만 알고 있었습니다:
- 캐시 읽기 (Cache read, hit): 입력 요율의 0.1배만큼 낮은 가격으로 청구됩니다. 이것이 모두가 인용하는 수치입니다. 캐시된 토큰(tokens)에 대해 약 90% 할인되는 셈이죠. 좋습니다.
- 캐시 쓰기 (Cache write): 프리미엄 (premium) 요금이 부과됩니다 — 짧은 (5분) 윈도우(window) 동안 입력 요율의 약 1.25배입니다. 이것은 아무도 언급하지 않는 수치입니다.
따라서 접두사 (prefix)를 처음 보낼 때 — 또는 만료될 때마다 — 90%를 절약하는 것이 아니라, 이를 저장하기 위해 25%의 _추가 요금 (surcharge)_을 지불하게 됩니다. 캐싱 (caching)은 동일한 접두사가 단순히 쓰이는 것이 아니라, 다시 읽힐 때 비로소 이득이 됩니다. 좋은 소식은, 쓰기 비용이 일반 읽기보다 약 0.25배 더 비싸고 (1.25x vs 1x), 단 한 번의 캐시 히트 (cache hit)만으로도 0.9x를 절약하기 때문에 (0.1x vs 1x) — _첫 번째 재사용_만으로도 이미 추가 요금을 회수하며, 그 이후의 모든 히트 (hit)는 약 90% 할인된다는 점입니다. 문제는 실제로 히트 (hit)를 발생시켜야 한다는 것입니다.
즉, 실패 모드는 구체적이고 고약합니다: 캐시 엔트리 (cache entry)를 작성한 후, 한 번도 읽지 않는 것. 사용하지도 않을 저장 공간을 위해 추가 비용을 지불한 셈입니다. 모든 호출마다 그렇게 한다면, 즉시 버려질 것들을 캐싱하기 위해 추가 요금을 지불하는 기계를 만든 것입니다.
그것이 정확히 제가 저지른 버그였습니다.
내가 실제로 무엇을 잘못했나
나의 "안정적인" 시스템 접두사(system prefix)는 안정적이지 않았습니다. 그 상단 근처에 몇 달 전에 추가하고 잊어버렸던 다음 내용이 숨겨져 있었습니다:
Current session started: 2026-07-14T09:41:07Z
타임스탬프(timestamp)였습니다. 매 호출마다 새로 생성되었습니다. 이는 접두사가 매 호출마다 달라짐을 의미했고, 따라서 제공업체는 매번 성실하게 새로운 캐시 항목을 작성했습니다 — 즉, 전체 쓰기 프리미엄(full write premium)이 발생했습니다 — 그리고 두 접두사가 일치하는 경우가 단 한 번도 없었기에 캐시를 다시 읽어오는 일도 전혀 없었습니다. 저는 캐싱의 비싼 부분만 켜고, 저렴한 부분은 전혀 사용하지 못했던 것입니다.
실마리는 내내 응답(response) 안에 있었지만, 제가 보지 않았을 뿐입니다:
print(resp.usage)
# cache_creation_input_tokens: 3948 <- 매 호출마다 작성 중
# cache_read_input_tokens: 0 <- 전혀 읽지 못함. 이것이 바로 버그입니다.
cache_read가 0에 머물러 있는 동안 cache_creation이 상승하는 것은 망가진 접두사의 전형적인 특징입니다. 이 글에서 한 가지만 얻어가신다면: 이 두 필드를 로그로 남기고 주시하십시오.
해결책은 더 많은 캐싱이 아니라 규율입니다
캐싱은 호출 간에 **바이트 단위로 완전히 동일(byte-for-byte identical)**하며, 변하는 요소보다 앞에(before) 위치하는 접두사에 보상을 줍니다. 두 가지 규칙이 있습니다:
1. 정적 콘텐츠를 먼저, 동적 콘텐츠를 마지막에. 시스템 프롬프트(system prompt), 도구 정의(tool definitions), 길게 붙여넣은 문서 — 이 모든 것을 앞부분에 변하지 않게 배치하십시오. 사용자의 실제 발화, 타임스탬프, 요청당 ID — 이 모든 것은 캐시 영역 이후인 _끝(end)_에 위치해야 합니다. 변하는 모든 것은 캐시 중단점(cache breakpoint)의 _하류(downstream)_에 있어야 하며, 절대로 그 안에 있어서는 안 됩니다.
2. 접두사를 완전히 동일하게 유지하십시오. 타임스탬프, 순서가 바뀐 JSON 키, 시스템 블록에 대한 "도움이 되는" 요청당 주입(injection) 등을 허용하지 마십시오. 단 하나의 움직이는 문자만 있어도 접두사 전체가 일치하지 않게 됩니다.
Claude의 경우, 중단점(breakpoint)을 사용하여 안정적인 접두사가 끝나는 지점을 표시합니다:
resp = client.messages.create(
model="claude-opus-4-8",
system=[{
...
OpenAI 계열 모델(GPT-5.6 / 5.5)의 경우 약 1,024 토큰 이상부터 자동으로 적용됩니다. Claude와 동일한 원칙이 적용되며 별도의 중단점(breakpoint)은 없습니다. 즉, 정적인 내용을 앞부분에 배치하여 가장 긴 공통 접두사(longest common prefix)와 일치하도록 유지하면 됩니다. Claude와 다른 두 가지 차이점은 알아둘 가치가 있습니다. 첫째, 쓰기 프리미엄(write premium)이 없습니다 (OpenAI는 캐시를 생성하는 데 비용을 부과하지 않습니다). 둘째, 읽기 할인율이 Claude의 0.1배보다 작습니다. 여전히 비용은 절감되지만, 90%까지는 아닙니다. 이는 prompt_tokens_details.cached_tokens에서 확인할 수 있습니다.
나는 타임스탬프를 사용자 메시지(user message)로 옮기고, 호출 사이의 시스템 블록(system block)은 건드리지 않은 채 다시 실행했습니다. 두 번째 호출에서 cache_read_input_tokens가 활성화되었고, 해당 에이전트의 비용이 급격히 감소했습니다. 왜냐하면 이 에이전트는 동일한 헤드(head)를 20회 이상 루프하며 반복하는데, 이제 이 헤드는 20번 쓰이는 대신 한 번 쓰이고 20번 읽히기 때문입니다.
캐싱이 실제로 이득이 되는 경우 (그리고 그렇지 않은 경우)
이것은 마법이 아니라 비용 분할(amortization)의 문제입니다. 크고 동일한 접두사(prefix)를 여러 번 읽을 때 이득을 봅니다:
- 에이전트 / 도구 루프 (Agent / tool loops) — 매 단계마다 동일한 시스템 프롬프트(system prompt)와 도구 스키마(tool schemas)를 다시 보냅니다. 이상적입니다.
- 긴 문서 Q&A (Long-doc Q&A) — 문서를 한 번 붙여넣고, 그 문서에 대해 열 가지 질문을 던집니다.
- 대량 추출 (Bulk extraction) — 방대한 지침(instruction) + 퓨샷 블록(few-shot block)을 사용하며, 입력 행(input row)만 변경됩니다.
프롬프트 전체가 매번 달라지는 원샷 호출(one-shot calls)에서는 아무런 효과가 없습니다. 분할할 반복적인 접두사가 없기 때문입니다. Claude의 경우, 이런 상황에서 명시적으로 캐싱을 시도하는 것은 아무런 이득 없이 1.25배의 쓰기 비용만 낭비하는 꼴입니다. OpenAI는 시도 자체에 비용을 부과하지는 않지만, 도움이 되지도 않습니다. 워크로드에 맞춰 도구를 선택하세요.
게이트웨이 주의사항, 내가 하나를 사용하고 있으므로
저는 제공업체(providers)를 직접 호출하지 않습니다. 게이트웨이를 통해 라우팅합니다 (저는 byesu를 사용하며, 하나의 키로 Claude/GPT/Grok에 대해 종량제(pay-as-you-go) 방식으로 이용합니다). 이로 인해 언급할 가치가 있는 하나의 장애 모드(failure mode)가 추가됩니다: 일부 애그리게이터(aggregators)는 캐싱을 조용히 깨뜨립니다. 만약 프록시(proxy)가 필드 순서를 재정렬하거나, cache_control을 제거하거나, 프롬프트 본문에 요청당 헤더(per-request header)를 삽입하면, 접두사(prefix)가 일치하지 않게 됩니다. 그러면 캐시 히트(cache hits)가 전혀 없는 답변을 받게 되지만, 이를 알려주는 에러는 발생하지 않습니다. 당신은 그저 조용히 전체 비용을 지불하게 될 뿐입니다.
따라서 사용 중인 게이트웨이에서 이를 구체적으로 테스트하십시오: 동일한 요청을 두 번 보내고, 두 번째 요청에서 cache_read_input_tokens가 0이 아닌지 확인하세요. 제가 사용하는 서비스의 경우, cache_control과 사용량(usage) 필드가 공식 업스트림(upstream)으로 변경 없이 전달되므로, 제가 생성한 캐시 키가 실제로 사용되며 0.1x 읽기 비용이 나타납니다. 하지만 "내 프록시가 캐싱을 보존하는가"라는 질문은 저를 포함한 누구의 말도 맹신하지 말고, 사용량 객체(usage object)를 통해 직접 확인해야 하는 질문입니다.
세 줄로 요약한 전체 교훈
- 캐싱에는 쓰기 프리미엄(write premium) (~1.25x)이 있습니다. 읽기는 저렴합니다 (0.1x). 이는 분할 상환(amortization)과 같습니다. 즉, 반복적인 읽기가 필요합니다.
- 접두사(prefix)는 반드시 바이트 단위로 안정적(byte-stable)이어야 하며 맨 앞에 위치해야 합니다. 시스템 프롬프트에 포함된 타임스탬프(timestamp)는 조용히 당신의 돈을 갉아먹을 것입니다.
cache_read_input_tokens를 주시하세요. 반복 호출 시 이 값이 0이라면, 마케팅에서 무엇이라 말하든 캐싱이 작동하지 않는 것입니다.
참조 버전 — 임계점 규칙(breakpoint rules), OpenAI 측 내용, 제공업체별 정확한 사용량 필드 — 이 필요하시다면 여기에 정리해 두었습니다. 하지만 위에서 언급한 타임스탬프 이야기가 사실상 교훈의 전부입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기