"Claude Code --continue/--resume 프롬프트 캐시 무효화 — v2.1.116에서도 재현"
TTL-matched 대조군으로 분리해낸 세션 재개 이벤트의 캐시 파괴
핵심 요약
- 현상:
claude --continue또는--resume으로 세션을 재개하면 1시간 TTL 이내임에도 프롬프트 캐시가 무효화된다. v2.1.69에서 도입된deferred_tools_delta가 원인으로 지목되며, v2.1.90~92 패치 이후에도 v2.1.116에서 재현이 확인된다. - 측정 방법: 동일한 유휴 구간(idle gap)에서 세션 내 연속 턴(베이스라인)과 세션 재개 직후 첫 턴(테스트)의 캐시 히트율을 비교하는 TTL-matched 대조군 방법론으로 TTL 만료 요인을 배제하였다. 결과: ~4분 구간 −56pp, ~28분 구간 −99pp.
- 대응: GitHub 이슈 #51764 추적, 커스텀 에이전트+MCP+스킬 조합 경로에서의 수정 범위 확인 요청,
--no-deferred-tools-deltaopt-out 플래그 도입 제안.
들어가며 — 문제 정의
Claude Code는 장기 실행 에이전트 환경에서 프롬프트 캐싱(Prompt Caching)에 크게 의존한다. 100k 토큰 이상의 컨텍스트를 매 턴마다 재전송할 때, 캐시 히트가 보장되면 cache_read 비용만 발생하고 cache_creation(쓰기 프리미엄)은 최소화된다. Anthropic 공식 문서 기준 1시간 캐시는 쓰기 시 기본 입력 토큰 비용의 2×(쓰기 프리미엄), 읽기 시 0.1×가 적용된다. 정상 운영 세션에서 히트율 95~99%가 관측되는 구조가 유지된다면, 동일한 컨텍스트 규모에서 토큰 비용의 대부분이 절감된다.
이 구조가 --continue/--resume 시 무너진다. v2.1.69에서 deferred_tools_delta 기능이 도입된 이후, 세션 재개 시 툴 결과 블록과 어태치먼트 블록의 재정렬이 발생하고 이것이 서버의 캐시 키를 변경하는 것으로 지목되었다. GitHub 이슈 #42338이 이 문제를 처음 보고하였고, v2.1.90/91/92 패치 이후 종료 처리되었다. 커뮤니티 기여자 ArkNill의 광범위한 캐시 분석(참조 5)과 simpolism의 브레이킹 어태치먼트 세트 식별 gist(참조 6), 그리고 taekim34의 환경 재현 리포트가 당시 이슈의 주요 근거를 형성했다.
그러나 v2.1.116에서도 동일한 현상이 재현된다. 기존 보고들과의 차이점은 TTL-matched 대조군(TTL-matched control pairs) 방법론을 통해 TTL 만료를 원인에서 배제하고 세션 재개 이벤트 자체를 캐시 파괴의 직접 원인으로 격리했다는 점이다. 이 방법론은 "캐시가 깨졌다"는 단순 관측을 넘어, 동일 조건 비교를 통해 재개 이벤트를 독립 변수로 처리한다.
방법론 — TTL-matched 대조군
기존 보고들(#42338)은 단일 케이스 토큰 카운트 방식("2~12초 exit→resume에서 470~512k cache_creation 발생")을 사용했다. 이 방식은 "5분 TTL이 마침 만료된 것 아닌가?"라는 반론을 배제하기 어렵다. Claude Code는 내부적으로 5분 TTL이 아닌 1시간 TTL을 사용한다는 사실(관측 1)을 고려해도, "1시간 TTL이 마침 만료됐을 수 있다"는 반론은 여전히 가능하다.
TTL 요인을 완전히 제거하기 위해 14개의 jsonl 세션 파일에서 per-turn usage 데이터를 수집하였다. requestId 기준 중복 제거로 반복 카운팅을 제거한 후, 두 비교 세트를 구성하였다.
- 베이스라인: 동일 jsonl 내 연속 턴, 유휴 구간 3~28분
- 테스트: 이전 jsonl 종료 → 새 jsonl 시작(세션 재개), 동일 유휴 구간
두 세트에서 캐시 히트율 차이가 관측된다면, 이는 TTL로 설명할 수 없다. 유휴 구간이 동일하기 때문이다. 두 케이스 간 제어된 변수는 "세션 경계를 넘었는가"뿐이므로, 차이는 세션 재개 이벤트 자체에 귀속된다. 이 설계는 인과 관계를 단순 관측이 아닌 대조 실험으로 지지한다.
관측 결과
관측 1 — Claude Code 내부 캐시 TTL
ephemeral_5m_input_tokens는 전 구간에서 0이다. 모든 cache_creation은 ephemeral_1h_input_tokens로 흐른다. Claude Code는 내부적으로 1시간 TTL 캐시를 사용한다. 따라서 ~60분 이내의 세션 내 유휴는 캐시를 유지해야 한다.
관측 2 — 베이스라인(세션 내, 동일 jsonl)
| 유휴 구간 | cache_creation | cache_read | 히트율 |
|---|---|---|---|
| 3.3분 | 389 | 155,492 | 99.8% |
| 3.3분 | 376 | 80,990 | 99.5% |
| 3.7분 | 2,123 | 159,700 | 98.7% |
| 4.0분 | 3,209 | 107,456 | 97.1% |
| 4.0분 | 5,112 | 173,721 | 97.1% |
| 4.3분 | 2,015 | 220,492 | 99.1% |
| 4.8분 | 840 | 78,457 | 98.9% |
| 5.7분 | 264 | 82,612 | 99.7% |
| 5.8분 | 3,302 | 85,549 | 96.3% |
| 5.9분 | 1,283 | 240,818 | 99.5% |
| 6.0분 | 2,385 | 161,823 | 98.5% |
| 7.6분 | 4,945 | 114,763 | 95.9% |
| 27.4분 | 1,559 | 150,593 | 99.0% |
평균 약 98% 히트율. 세션 프로세스가 살아있는 동안 1시간 캐시는 정상 동작한다.
관측 3 — 세션 재개 전이(동일 에이전트, 새 jsonl 파일)
| 전이 | 유휴 구간 | 다음 cc | 다음 cr | 히트율 | 판정 |
|---|---|---|---|---|---|
| f3f5c819 → e17536e7 | 28.2분 | 40,260 | 0 | 0.0% | 예상 외 미스 |
| e17536e7 → 096d96fa | 4.1분 | 23,849 | 16,702 | 41.2% | 예상 외 미스 |
| c0eb34ad → b6e07fac | 2423.6분 (>1h TTL) | 20,061 | 0 | 0.0% | TTL 만료, 정상 |
| b6e07fac → f3f5c819 | 2059.3분 (>1h TTL) | 41,766 | 0 | 0.0% | TTL 만료, 정상 |
TTL-matched 비교 — 직접 델타
| 유휴 구간 | 세션 내(베이스라인) | 세션 재개(테스트) | 델타 |
|---|---|---|---|
| ~4분 | 97~99% 히트 (cr > 100k, cc ≈ 3k) | 41% 히트 (cr 16.7k, cc 23.8k) | −56pp |
| ~28분 | 99% 히트 (cr 150k, cc 1.5k) | 0% 히트 (cr 0, cc 40.3k) | −99pp |
동일한 유휴 구간, 동일한 에이전트 워크로드, 동일한 코드베이스 상태. 유일한 차이는 "세션 경계 통과 여부"다. 이 델타는 세션 재개 이벤트 자체가 캐시를 파괴한다는 결론을 지지한다.
원인 정렬 — deferred_tools_delta 재정렬 가설
42338은 v2.1.69에서 도입된 deferred_tools_delta를 근인으로 지목했다. 이 기능은 세션 재개 시 롤아웃 리플레이 과정에서 툴 결과 블록과 어태치먼트 블록을 재정렬한다. 블록 순서가 달라지면 요청 바이트 prefix가 변경되고, Anthropic 서버는 prefix 일치로 캐시 키를 조회하므로 key miss가 발생한다.
관측 결과는 이 가설과 일치한다. 4.1분 재개 케이스는 부분 prefix 매칭(16,702 토큰 cache_read + 23,849 토큰 cache_creation)을 보인다. 이는 "블록 N까지는 prefix가 일치하다가, 재정렬된 지점에서 캐시 키가 깨지는" 패턴과 일치한다. 순수 TTL 만료라면 0% 히트가 발생하며 부분 매칭은 없어야 한다. 부분 매칭이 존재한다는 사실 자체가 TTL 가설을 기각하는 내부 증거다.
28.2분 케이스는 0% 히트를 보인다. 재정렬이 prefix의 더 앞쪽 블록에서 발생했음을 시사한다. simpolism이 분석한 gist(참조 6)에서 목록화된 skill_listing, todo_reminders, nested_memory 인젝션의 위치 변화 패턴과 일치한다. 재개 시 어느 블록이 먼저 재정렬되는지에 따라 "부분 매칭(히트율 41%)"과 "전체 미스(0%)"의 차이가 결정된다.
현재 v2.1.90~92 패치가 이 경로 전체를 수정했는지는 불명확하다. 특히 커스텀 에이전트, MCP 서버 다수, 스킬, 훅의 조합 환경에서 발생하는 블록 구성 패턴이 단순 환경에서의 패치 범위를 벗어날 수 있다.
영향 — 실제 비용 측정
하나의 11시간 52분 세션(197 유니크 턴)에서의 usage 집계:
| 항목 | 수치 |
|---|---|
| cache_read 합계 | 60,869,087 토큰 |
| cache_creation 합계 | 2,017,110 토큰 (전량 1시간, 2× 쓰기 프리미엄) |
| input_tokens 합계 | 9,923 토큰 |
| output_tokens 합계 | 446,687 토큰 |
| cc / output 비율 | 4.52× |
정상 캐시 운영 세션의 cc/output 비율은 통상 1~2× 수준으로 관측된다. 이 세션의 4.52×는 비정상적으로 높다. 세션 내에는 1시간 TTL을 초과한 정직한 만료 전이(>1h) 2건이 포함되어 있으며, 이는 정상 비용이다. 그러나 f3f5c819 → e17536e7 전이(28.2분, 40,260 cc)는 TTL 이내의 재개임에도 전체 캐시 미스가 발생했다. 해당 40,260 토큰의 cache_creation은 프리픽스가 보존되는 재개였다면 발생하지 않았을 소비다.
1시간 캐시의 cache_creation은 기본 입력 대비 2× 비용이 적용된다. 40,260 토큰 규모의 불필요한 쓰기가 매 세션 재개마다 반복되면, 잦은 자동 재시작이 포함된 에이전트 운영에서는 누적 비용이 체감 수준에 이른다.
구독 플랜 사용자에게는 이 비용이 롤링 5시간 할당량에서 직접 차감된다. cache_creation은 cache_read보다 할당량을 더 빠르게 소진한다. 야간 에이전트 자동 재시작 패턴에서는 재개 유발 cache_creation이 할당량의 상당 부분을 점유할 수 있으며, 이는 실제 작업 처리에 사용할 수 있는 할당량을 감소시킨다.
대응 제안
이슈 상태 확인 및 추적: GitHub 이슈 #42338은 v2.1.90~92 패치 후 종료 처리되었으나 v2.1.116에서 재현이 확인되었다. 이슈 #51764로 신규 리포트가 제출된 상태다. 동일 현상을 경험하는 사용자는 해당 이슈에 재현 환경과 버전을 추가하는 것이 효과적이다. 재현 사례가 쌓일수록 패치 우선순위에 영향을 줄 수 있다.
적용 범위 확인: v2.1.90~92 패치의 공식 릴리스 노트에는 수정 범위가 구체적으로 명시되지 않았다. 커스텀 에이전트 + MCP 서버 조합 + 스킬 + 훅이 포함된 환경에서 발생하는 블록 구성 패턴이 단순 환경 기준의 패치 범위에서 누락되었을 가능성이 있다. 해당 조합을 사용하는 사용자는 직접 재현 테스트를 통해 확인하거나, 이슈에 환경 정보를 추가하여 Anthropic 측에 확인 요청을 하는 것이 권장된다.
--no-deferred-tools-delta opt-out 플래그 요청: deferred_tools_delta 기능 자체가 성능 개선을 목적으로 도입되었음을 고려할 때, 이를 완전히 제거하는 방향보다는 opt-out 플래그를 제공하는 방향이 현실적이다. 하네스 사용자나 장기 에이전트 운영자에게는 기능 완전성보다 캐시 안정성이 우선일 수 있다. --no-deferred-tools-delta 또는 --strict-prefix 형태의 플래그 도입 요청이 이슈 #51764의 Asks 항목에 포함되어 있다.
재현 절차
- Claude Code 세션을 시작하고 CLAUDE.md, 툴 정의, 이전 대화 히스토리 등을 포함한 컨텍스트를 ≥100k 토큰까지 누적한다.
/exit으로 세션을 종료하고, 1~5분 이내에claude --continue로 재개한다. 재개 간격은 1시간 TTL 이내로 유지해야 한다.- 재개된 세션의 첫 번째 어시스턴트 턴에서
~/.claude/projects/<slug>/<sid>.jsonl을 열고 해당requestId의usage객체 내cache_read_input_tokens와cache_creation_input_tokens를 확인한다. 모니터링 프록시가 있는 경우 per-turn usage 조회도 가능하다. - 대조군 수집: 같은 세션에서
/exit없이 동일한 유휴 구간을 두고 다음 턴을 실행한다. 같은 jsonl 파일 내의 연속 턴이어야 한다. 베이스라인에서는 ≥95% cache_read가 관측되어야 한다. - 두 케이스의 히트율을 비교한다. 재개 후 히트율이 유의미하게 낮다면(특히 0~41% 수준), 해당 버그가 재현된 것으로 판단할 수 있다. 이슈 #51764에 재현 환경(버전, 활성 기능 목록)을 추가하는 것이 커뮤니티에 도움이 된다.
결론
deferred_tools_delta 기반 캐시 파괴 버그는 v2.1.116에서도 재현된다. TTL-matched 대조군 방법론은 TTL 만료를 변수에서 배제함으로써, 캐시 파괴의 원인이 세션 재개 이벤트 자체에 있음을 구조적으로 지지한다. ~4분 구간에서 −56pp, ~28분 구간에서 −99pp의 히트율 델타는 단순 성능 저하가 아니라 기능적 캐시 무효화다.
이 문제가 실질적으로 영향을 미치는 패턴은 다음과 같다. 첫째, /compact 또는 에러로 인한 세션 재시작이 빈번한 장기 에이전트. 둘째, 야간 자동 재시작 구조를 가진 모니터링 에이전트. 셋째, 100k 이상의 컨텍스트를 유지하는 복잡한 하네스 환경.
Anthropic이 v2.1.90~92에서 패치를 시도했다는 사실은 문제의 실재성을 확인한다. v2.1.116에서의 재현은 수정이 불완전하거나, 특정 환경 조합에서 재발하는 경로가 남아있음을 시사한다. 이슈 #51764가 추적 가능한 상태로 유지되어 이 문제가 해결되기를 기대한다.
참조
- GitHub Issue #42338 — 원본 리포트 (종료, 잠금): https://github.com/anthropics/claude-code/issues/42338
- GitHub Issue #34629 — v2.1.69 이후
--print --resume회귀: https://github.com/anthropics/claude-code/issues/34629 - GitHub Issue #46829 — 1h→5m 기본 TTL 무음 회귀: https://github.com/anthropics/claude-code/issues/46829
- GitHub Issue #51764 — 본 리포트 (오픈): https://github.com/anthropics/claude-code/issues/51764
- ArkNill — Claude Code 캐시 분석: https://github.com/ArkNill/claude-code-cache-analysis
- simpolism — 브레이킹 어태치먼트 세트 분석 gist: https://gist.github.com/simpolism/302621e661f462f3e78684d96bf307ba
- Anthropic Prompt Caching 공식 문서: https://platform.claude.com/docs/en/build-with-claude/prompt-caching
댓글
댓글 쓰기