원래 my Substack에 게시되었습니다.
Amazon Bedrock에서 프롬프트 캐싱이 단 하나의 오류도 없이 작동을 멈출 수 있습니다. 저는 Bedrock에서 Claude 에이전트 플랫폼을 운영하고 있는데, 이번 분기 동안 대형 에이전트 프레임워크의 Bedrock 캐싱에 대한 버그 보고서를 검토했습니다. 다섯 가지 실패 사례가 계속해서 나타났습니다. 각 사례별로 무엇이 발생하는지, 자신에게도 발생하고 있는지 어떻게 알 수 있는지, 그리고 무엇을 변경해야 하는지를 설명합니다.
7월에 LiteLLM 업그레이드가 Bedrock 요청에서 시스템 프롬프트가 들어가는 위치를 변경했습니다. Claude Code 트래픽의 캐시 적중률(Cache hits)이 약 90%에서 2545%로 떨어졌고, 아무도 원인을 파악하기 전까지 6일 동안 일일 지출액이 23배 증가했습니다. 모든 요청은 200을 반환했습니다. AWS는 캐싱 규칙에 대해 문서화하고 있지만, 에이전트에게 가장 큰 타격을 주는 부분인 Bedrock이 이전 캐시 항목을 얼마나 멀리까지 검색하는지에 대해서는
에이전트 루프(agent loop)에서 대부분의 새로운 토큰은 사용자 메시지가 아니라 도구 호출 및 도구 결과입니다. 만약 캐시 포인트가 마지막 사용자 메시지에만 위치한다면, 도구 기록(tool history)이 뒤로 쌓이는 동안 첫 번째 프롬프트에 머무르게 됩니다. 그러면 매번 호출할 때마다 그 전체 기록에 대한 비용을 지불하게 됩니다.
Cline 4.1.0에서 이런 일이 발생했습니다. 이 버전의 Bedrock 코드는 캐시 포인트를 역할(role)이 “user”인 마지막 메시지에 두었고, 메시지 형식에서 도구 결과는 자체적인 역할을 가집니다. 따라서 포인트가 초기 프롬프트를 지나서 절대 이동하지 않았습니다. 사용자들은 세션 시작 시 한 번만 캐시 쓰기(cache write)를 확인했고 그 이후로는 아무것도 보지 못했습니다(cline/cline#13738; 4.0.12 버전은 괜찮았습니다).
테스트: 긴 에이전트 세션에서 캐시 읽기(cache reads)는 호출마다 증가해야 합니다. 만약 입력 토큰이 계속 늘어나는데도 불구하고 그것들이 증가하는 것이 멈춘다면, 포인트가 이동하지 않는 것입니다. 해결책은 역할에 관계없이 가장 최신 메시지의 마지막 블록에 캐시 포인트를 두는 것입니다.
Bedrock이 더 이른 캐시 항목을 찾으려고 할 때, 새로운 캐시 포인트로부터 고정된 수의 콘텐츠 블록만 검색합니다. 저는 경계값을 측정해 보았는데, 이전 캐시 포인트 이후 21개의 블록이 추가되었을 때는 다음 요청이 여전히 적중했지만, 22개가 되자 매번 놓쳤습니다. 모든 도구 호출과 모든 도구 결과는 하나의 블록입니다. 따라서 열 개 이상의 병렬 도구 호출로 이루어진 한 번의 턴(turn)만으로도 이전 항목이 검색 범위를 벗어날 수 있습니다.
[Bar chart: with 19, 20 or 21 content blocks added, Bedrock read about 2,900 tokens from cache. With 22, 23 or 24, it read 0.](https://substackcdn.com/image/fetch/$s_!PEMl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F46211289-171b-4055-b892-dc8894c3b05e_1080x1350.png
pydantic-ai도 자체 테스트에서 같은 문제를 발견했습니다. 10번의 병렬 호출(21개 새 블록) 후 다음 요청은 캐시에서 6,827 토큰을 읽었습니다. 11번 호출(23개 블록) 후에는 0을 읽고 7,534를 다시 작성했습니다(pydantic/pydantic-ai#9404). Strands 사용자는 18번의 호출 턴 이후 0 토큰을 읽고 134,082를 작성하는 것을 확인했습니다(strands-agents/harness-sdk#3348). Anthropic 자체 API에서 같은 요청들은 누락되지 않았는데, 도구 호출(tool calls) 연속이 하나의 위치로 계산되기 때문입니다. 하지만 Bedrock은 각각을 계산합니다.
테스트 방법: 가장 넓었던 턴 직후의 호출들을 살펴보세요. 다른 곳에서는 정상적으로 읽히지만 여기서 0을 읽는다면 이것이 문제입니다. 해결책: 턴에서 약 18개 블록보다 더 많은 내용을 추가했을 때 이전 캐시 지점을 유지하세요. Bedrock은 요청당 네 개의 캐시 포인트를 허용하므로 보통 여유 공간이 있습니다.
3. 캐시 포인트 이후 변경되는 모든 것을 유지하기
캐싱은 접두사 일치(prefix match) 방식입니다. 날짜, 요청 ID, 사용자 이름 또는 캐시 포인트 이전에 배치된 항목별 지침 같은 것이 모든 요청을 고유하게 만들어, 모든 호출이 캐시를 작성하고 아무것도 읽지 못하게 만듭니다.
AWS 샘플 앱에서 이를 명확히 보여주었습니다. 동일한 문서를 검토하는 28회 이상의 호출 동안 CloudWatch는 캐시에 196,680 토큰을 작성하고 0을 읽은 것으로 기록했습니다. 이는 캐시 포인트가 각 항목의 자체 지침 이후에 배치되었기 때문입니다. 이를 문서 직후로 옮기자 두 번째 항목이 4,624 토큰을 읽는 것(read)으로 바뀌었습니다(aws-samples/review-and-assessment-powered-by-intelligent-documentation#327). 더 미묘한 버전도 존재합니다. Mastra 설정 중 하나에서는 메시지를 Postgres jsonb 컬럼에 저장하는 것이 나중에 속성 키의 순서를 변경시켰고, 이는 프롬프트 바이트를 변경하여 캐시가 깨지게 만들었습니다(mastra-ai/mastra#25929).
테스트: 동일한 요청을 두 번 실행하고 캐시 지점까지의 바이트를 비교합니다. 이들은 동일해야 합니다. 해결책은 마지막 캐시 지점 뒤에 동적 텍스트를 추가하고, 구조화된 모든 것을 안정적인 순서로 직렬화하는 것입니다.
4. 라이브러리가 여전히 캐시 지점을 전송하는지 확인하세요
대부분의 팀은 캐시 지점을 수동으로 작성하지 않습니다. 프레임워크나 게이트웨이가 추가하며, 종종 인식하는 모델 ID에 대해서만 그렇게 합니다. 라이브러리를 업그레이드하거나 새로운 모델 ID 또는 애플리케이션 추론 프로필 ARN으로 전환하면 캐시 지점이 조용히 사라질 수 있습니다.
이것이 위에서 언급된 LiteLLM 사례입니다. 드문 일이 아닙니다: MLflow의 AI Gateway는 Bedrock 채팅 메시지를 일반 문자열로 변환하여 아예 캐시 지점을 전송하지 않습니다(mlflow/mlflow#26477), 그리고 LangChain의 openwiki는 캐싱 미들웨어가 애플리케이션 추론 프로필 ARN을 건너뛰는 경우를 발견했습니다(langchain-ai/openwiki#853).
테스트: 캐시 읽기와 캐시 쓰기가 모두 0이면, Bedrock에 아무런 캐시 지점도 도달하지 않은 것입니다. 모든 라이브러리 업그레이드와 모델 변경 후에 이를 확인하세요.
5. 스트리밍이 메시지를 재구성하는 방식을 관찰하세요
응답을 스트리밍하고 대화에 다시 저장할 때, 라이브러리는 청크로부터 어시스턴트 메시지를 재구성합니다. 만약 이것을 단일 텍스트 조각과 다르게 재구성한다면, 히스토리가 모양이 바뀝니다.
LangChain.js에서는 Bedrock에서 확장된 사고(extended thinking)를 스트리밍할 때 블록 인덱스가 텍스트 청크에서 누락되어 하나의 답변이 수백 개의 별도 텍스트 블록으로 저장되었습니다. 이는 다음 캐시 지점을 조회 한계(lookback limit)보다 훨씬 뒤로 밀어냈습니다(langchain-ai/langchainjs#11357).
테스트: 저장된 어시스턴트 메시지의 콘텐츠 블록 수를 세어보세요. 하나의 답변은 수백 개가 아니라 하나의 텍스트 블록이어야 합니다.
자체 계정 확인하기
AWS/Bedrock 네임스페이스의 두 CloudWatch 메트릭인 CacheReadInputTokenCount와 CacheWriteInputTokenCount가 모델 ID별로 대부분을 알려줍니다. 트래픽이 안정적일 때는 읽기(reads) 횟수가 쓰기(writes) 횟수보다 훨씬 많아야 합니다. 읽기 횟수는 많은데 쓰기 횟수가 거의 0에 가깝다는 것은 캐싱은 작동하지만 누락되고 있다는 의미이며, 이는 아예 캐싱을 안 하는 것보다 더 비용이 많이 발생합니다. 둘 다 거의 0에 가깝다면 캐시 지점(cache point) 자체가 전송되지 않고 있다는 뜻입니다.
만약 프롬프트가 동일한데도 바쁜 시간에 히트율(hit rate)이 떨어진다면, 리전 간 추론(cross-region inference)을 확인해 보세요. AWS는 높은 수요 시점에 라우팅 과정에서 요청이 캐시된 사본을 보유하지 않은 리전에 도착할 수 있어 “캐시 쓰기 증가”를 야기할 수 있다고 언급합니다.
원인을 파악하려면 Bedrock 모델 호출 로깅(model invocation logging)을 활성화하세요. 이 로그에는 모든 호출에 대한 캐시 카운트와 누가 요청했는지 정보가 담겨 있습니다.
제가 여전히 바라는 것들
두 가지입니다. 첫째, Bedrock의 캐시 진단 기능입니다. Anthropic의 콘솔은 요청이 왜 캐시에 실패했는지 설명해 주지만, Bedrock은 그렇지 않습니다. 둘째, AWS 문서에 “대략 20” 대신 정확한 조회 기간(lookback number)을 명시해 주었으면 합니다. 왜냐하면 도구 호출 하나만으로 에이전트가 히스토리 비용을 0.1x로 지불할지 1.25x로 지불할지가 결정되기 때문입니다.
출처 및 고지 사항
룩백(lookback) 수치는 제가 직접 측정한 것입니다. Amazon Bedrock에서 사이즈별로 두 번의 요청을 보냈고, Claude Sonnet 4.6 모델과 us-west-2 리전을 사용했으며, 2026년 10월에 각각 새 프롬프트로 두 번씩 실행하여 오래된 캐시 항목이 히트(hit)처럼 속이는 일이 없도록 했습니다(scripts/live_lookback_boundary.py, CacheCanary 리포지토리 참조). 가격과 승수(multipliers)는 AWS가 발표한 온디맨드 요율입니다. 나머지 수치들은 LiteLLM의 사고 보고서(docs.litellm.ai/blog/bedrock-invoke-prompt-caching-incident); Datadog, “Monitor prompt caching to optimize your token usage” (2026년 9월); Anthropic, “Reducing cost and improving performance with Claude Platform” (2026년 9월); 그리고 AWS Bedrock 프롬프트 캐싱 문서와 각 섹션에 링크된 공개 GitHub 이슈들에서 가져왔습니다.
저는 Ruby AI에서 에이전트 플랫폼을 구축하고 있으며, 이 플랫폼은 Bedrock에서 Claude를 실행합니다. 또한 위 체크 사항들을 위한 작은 무료 도구인 CacheCanary(github.com/Haarris/cachecanary)도 만들었습니다. 이 도구는 호출 로그(invocation logs)를 읽어 히트율과 미스(misses)가 얼마나 비용이 들었는지 보여주며, 저장된 두 요청을 비교하여 왜 두 번째 요청이 미스가 되었는지 알려줍니다. 이 게시물을 작성한 이후로는 LiteLLM, Strands Agents 및 LangChain에 대한 테스트 가이드도 작성했습니다. 이 가이드는 각각 무엇을 Bedrock으로 보내는지, 그리고 캐시가 어디서 손실되는지를 정확히 보여줍니다.
저는 프로덕션 환경에서 무언가가 고장 나고 문서가 경고하지 않았을 때 이런 글들을 씁니다. 다음 글은 먼저 제 Substack에 올라갈 예정입니다.