
Claude Fable 5의 토큰을 낭비하는 범인은 실증 결과 판명 — output이 아니라 cache read였다
요약
Claude Code 사용 시 토큰 소비의 약 95%가 output이 아닌 cache read에 집중됨을 실측 데이터로 증명했습니다. 긴 세션에서 발생하는 과도한 캐시 재독 문제를 분석하고, 이를 최적화하기 위한 지표 설정 및 세션 압축 방안을 다룹니다.
핵심 포인트
- 토큰 소비의 주범은 output이 아닌 비대해진 세션의 cache read임
- cache read는 단가는 낮으나 긴 세션에서 양이 기하급수적으로 증가함
- JSONL 로그 분석 시 requestId 중복 제거 등 정교한 집계가 필요함
- '/compact' 명령어를 통해 세션 컨텍스트를 중앙값 66% 축소 가능
2026-07-05 전면 개정: 초출(7/2)부터 추가를 2회 거친 시계열 구성을, 실측 데이터가 모두 갖춰진 시점에 체계적인 구성으로 다시 정리했습니다. 수치는 모두 전일 확정값으로 업데이트했습니다. 속보 당시의 경위나 생데이터(raw data)는 리포지토리의 docs/verification-2026-07-04.md / docs/verification-2026-07-05.md에 남겨두었습니다.
Claude Code를 돌린 실측 결과, 소비 토큰량의
약 95%가 cache read (캐시된 컨텍스트의 재독) 였습니다.
처음에는 "output이 무거운 것이 아닌가"라고 생각했습니다.
하지만 실제 output은 전체의 약 0.4%였습니다. 대부분을 차지한 것은 비대해진 세션을 매 턴마다 재독하는 cache read, 바로 이 녀석이 범인이었습니다.
다만, 이것이 "요금의 95%가 cache read였다"는 의미는 아닙니다. cache read는 일반적인 입력보다 단가가 저렴합니다. 문제는 단가가 아니라 양입니다. 긴 세션에서는 매 턴의 재독량이 눈덩이처럼 불어납니다.
이 기사에서 다루는 것은 저의 Claude Code 이용 로그(2026-07-02~07-05의 4일간)에서 나타난 실측 데이터입니다.
Claude Code 전체의 일반 통계는 아닙니다.
하지만 장시간 세션을 많이 사용하는 사람에게는 상당히 재현성 있는 문제가 될 것으로 보입니다.
이 기사의 전체 모습은 5가지입니다.
- 토큰 최적화에서 우선 봐야 할 것은 output이 아니라 cache read
- 집계에는 3가지 함정이 있음:
requestId의 중복,subagents/의 별도 파일, 그리고 측정 당일의 부분 일 단위 버킷(partial day bucket) - 시각화만으로는 행동이 변하지 않는다. 효과가 있었던 것은 후크(hook)를 통해 모델 스스로에게 전환 시점을 제안하게 하는 closed-loop
/compact는 세션 내의 live context를 중앙값 66% 축소 (실측 29건). 단, 200k 토큰을 넘기지 않으면 거의 무의미함- 4일간의 워크로드 정규화 시계열에서, 세션 중앙값의 cacheRead/output 비율이 233x → 83x로 단조 개선. 반면 cacheRd%와 hot rate는 움직이지 않았으며, "어떤 지표를 성공 지표로 삼아야 하는가" 자체가 실측의 성과였음
이 메커니즘은 Claude Code 플러그인 session-health로 공개하고 있습니다.
Fable 5의 부활에 환호하며 새벽 4시부터 Claude Code를 돌립니다.
구독 서비스라고는 해도 1주일의 Rate Limit이 있습니다.
"어떤 처리가 토큰을 가장 많이 잡아먹고 있는가"가 신경 쓰이시죠?
Claude Code는 ~/.claude/projects/ 하위에 세션의 트랜스크립트(transcript)를 JSONL 형식으로 남깁니다. 즉, API 측의 청구 화면을 기다리지 않아도 로컬에서만 input / output / cacheRead / cacheCreation 내역을 상당히 정확하게 뽑아낼 수 있습니다.
처음에는 "생성이 무거운 것이 아닌가"라고 생각했습니다.
실측해보니 주인공은 전혀 달랐습니다.
JSONL의 assistant 레코드에는 message.usage가 들어있습니다.
주요 내역은 다음 4가지입니다.
input_tokens
output_tokens
cache_read_input_tokens
cache_creation_input_tokens
여기에 추가로 requestId나 sessionId도 들어있습니다.
이 정보를 집계하면 세션별 토큰 내역을 뽑을 수 있습니다.
다만, 단순하게 합산하면 함정에 빠집니다. 저는 3가지를 겪었습니다.
1개 리퀘스트가 여러 개의 JSONL 행으로 나뉘어 기록되기 때문에, 행 단위로 합산하면 2~3배로 부풀려집니다...
requestId로 중복을 제거할 필요가 있었습니다.
첫날의 실측에서는 중복 제거를 통해 513.9M tokens 분량의 부풀려진 데이터를 제외했습니다.
이 부분을 틀리면 결론이 크게 어긋납니다.
또 다른 함정은 서브 에이전트(sub-agent)의 트랜스크립트입니다.
서브 에이전트의 로그는 본체와 같은 JSONL이 아니라, 다음과 같은 별도의 디렉토리에 저장됩니다.
<프로젝트>/<세션ID>/subagents/agent-*.jsonl
이 부분을 놓친 저는 "왜 위임(delegation)이 제로인 거야!!!"라며 의구심을 가졌습니다.
사실, main 이외의 agent에서도 875개 리퀘스트가 기록되어 있었습니다.
집계를 의심하고, 파일 구조를 확인한 뒤에 결론을 내렸어야 했다.
이것은 며칠간 운용하면서 빠진 함정이다.
7/4 낮 동안 집계한 「7/4의 행」은 총 tok 133M · 위임(delegation) 27%였다. 그런데 그 후에도 작업은 계속되었기에, 전체 일로 다시 집계하자 총 tok 220M · 위임 19%로 바뀌었다. 「7/4에 위임이 늘어났다」는 관찰은 낮 시간대의 편향에 불과했다.
일간 비교는 전체 일 확정 버킷(bucket)만으로 수행하고, 측정 당일 분은 「부분 일(partial day)」이라고 명기한다. 집계 툴을 만든 본인이 이 함정에 빠졌기에, 자책의 의미로 남겨둔다.
첫날(2026-07-02)의 1일 분량을 /session-health:usage-report로 집계.
조건은 다음과 같다.
- 12개 프로젝트
- 2,575개 리퀘스트
requestId베이스로 중복 제거 완료 - top-level session과subagents/하위의 transcript를 모두 집계 - 중복 제거로 제외된 부풀려진 분량: 513.9M tokens
결과.
| 종별 | 토큰 | 비율 |
|---|---|---|
| cache read | 344.0M | 95.3% |
| ... |
output은 약 0.4%.
반면, cache read는 약 95%.
Claude Code의 토큰 소비에서 지배적이었던 것은 생성 그 자체가 아니라, **길고 비대한 세션의 재독(re-reading)**이었다.
세션별로 보면, 최악의 구간에서는 cacheRead/output 비율이 200~300배 초과까지 불어나 있었다. 최대로는 313배.
cacheRd/out=313x
cacheRd/out=297x
cacheRd/out=293x
...
숫자를 보면, output을 조금 줄이는 것보다
우선 세션을 끊는 시점을 앞당기는 것이 더 효과적인 구조임을 알 수 있다.
프롬프트 캐시(Prompt Cache) 자체가 범인은 아니다.
Claude Code와 같이 긴 컨텍스트(long context)를 전제로 하는 워크플로우에서는 캐시는 오히려 필수(essential)에 가깝다. 매번 모든 것을 일반 입력으로 보내는 것보다 캐시를 사용하는 편이 더 저렴하다.
문제는 usage 상, 길고 비대한 세션일수록 각 리퀘스트에서 대량의 cache_read_input_tokens가 계상되는 구조라는 점이다.
세션이 길어진다.
컨텍스트가 비대해진다.
1턴마다의 재독량이 늘어난다.
거기에 대화를 더 이어간다.
그때마다 비대한 컨텍스트를 다시 읽는다.
이 반복으로 인해 cache read의 총량이 눈덩이처럼 불어난다.
즉, 토큰 최적화의 주전장은 「답변을 짧게 하는 것」만이 아니다.
오히려 길어진 세션을 어디서 접을(fold) 것인가가 중요해진다.
agent별 내역도 뽑아보았다.
결과, 첫날의 main-thread share는 **89%**였다.
이는 전체 토큰 소비의 대부분이 아직 main thread에 집중되어 있었다는 의미이다. 서브 에이전트(sub-agent)는 움직이고 있지만, 「메인은 오케스트레이터(orchestrator), 구현이나 조사는 서브 에이전트」라는 이상적인 형태에는 아직 도달하지 못했다.
서브 에이전트는 메인 대화의 컨텍스트를 더럽히지 않고 탐색이나 구현을 분리할 수 있다. 따라서 Claude Code를 장시간 사용한다면 유효한 설계라고 생각한다.
다만, 「서브 에이전트를 사용하고 있다」는 것만으로는 부족하다.
실제로는 다음과 같은 관점까지 살펴볼 필요가 있다.
- 어떤 agent가 움직이고 있는가
- 어떤 model로 움직이고 있는가
- main thread에 얼마나 남아 있는가
- 탐색이나 정형 작업이 정말로 위임되고 있는가
- 무거운 모델로 가벼운 작업을 돌리고 있지는 않은가
「탐색은 가벼운 모델로 넘기고 있다」고 생각하더라도, 실제로 어떤 model이 작동했는지는 로그를 보지 않으면 알 수 없다.
CLAUDE.md나 운용 규칙에 적어두는 것만으로는 불충분하며, 실측으로 확인할 필요가 있다.
사실, statusline에는 이미 「세션이 커지면 경고」를 심어두었다.
하지만 결과는 이랬다.
3.4MB까지 팽창.
인간은 경고를 보지 않는다.
적어도 나는 보지 않았다.
보았더라도 작업의 흐름이 끊기면 무시한다.
「조금만 더」라고 생각하며 계속한다.
그 「조금만 더」가 쌓여서 세션이 비대해진다.
그래서 발상을 전환했다.
인간에게 경고하는 것이 아니라, 모델 측이 자각하게 만든다.
Claude Code의 UserPromptSubmit
훅(Hook)은 사용자가 프롬프트를 전송한 직후, Claude가 처리하기 전에 스크립트를 끼워 넣을 수 있다. 여기서 additionalContext를 반환하면, 다음 모델 요청(request)에 컨텍스트로 주입할 수 있다.
session-health는 세션의 상태를 보고, 임계값(threshold)을 초과하면 모델에 짧은 교정 지시를 주입한다.
임계값 감지
- 요청(request) 80회 초과
- 또는 cacheRead/output 비율 150배 초과
...
주입은 20회 요청당 1회, 약 60토큰 정도다.
따라서 교정 비용 자체는 상당히 작다.
동일한 판정 엔진을 다음 세 가지에 연결했다.
| 출력처 | 역할 |
|---|---|
UserPromptSubmit hook | 모델에 전환 시점과 위임 방침을 주입한다 |
| statusline | 인간에게 현재의 session health를 표시한다 |
| Stop hook 통지 | 응답 완료 시 「전환 시점」을 통지한다 |
포인트는 단순한 대시보드가 아니라는 점이다.
감지하고 끝내는 것이 아니라, 모델의 다음 행동에 개입한다.
운영 초기 단계에서 툴 자체의 버그도 겪었기에 공유해 둔다.
당초에는 요청(req) 수와 cacheRead/output 비율을 트랜스크립트(transcript) 전체에서 누적하고 있었다. 그러다 보니 /compact를 해도 카운터가 내려가지 않았다. statusline은 🔥 상태로 유지되고, 훅은
관측 창은 7/2~7/5(폐루프(closed-loop) 가동 시작은 7/2 밤, 7/5는 15:30경까지의 부분적인 날)이다. 창 전체가 동일한 모델 세대이므로, 일간 비교에서 모델 이전에 따른 교란 요인은 없다.
| 날짜 | 총 토큰(Total tok) | cacheRd% | main share | 위임 | hot(≥80req) | 세션 수 |
|---|---|---|---|---|---|---|
| 7/2 (가동 전 중심) | 381M | 95% | 87% | 13% | 13 | 43 |
| ... |
총 토큰은 일별로 7배씩 변동(57M~381M)하므로, 총량의 증감 그 자체로는 아무것도 주장할 수 없다. 그리고 cacheRd%는 변하지 않는다. cache read가 9할을 차지하는 것은 이 워크로드(workload)의 구조일 뿐, 폐루프의 성공 지표는 아니라는 점이 시계열을 연장함으로써 명확해졌다.
따라서 세션 단위로 정규화한 지표로 살펴본다.
| 날짜 | 세션 중앙값 cacheRd/out | 세션 중앙값 req 수 | worst 비(절대량 병기) | hot률 |
|---|---|---|---|---|
| 7/2 | 233x | 2 | 313x (cacheRd 32.8M) | 30% |
| 7/3 | 130x | 59 | 249x (2.5M) | 38% |
| 7/4 | 110x | 59 | 891x (5.8M) | 44% |
| 7/5 (부분적 날) | 83x | 20 | 165x (23.5M) | 38% |
(cacheRd/out 비는 req≥10인 세션만을 대상으로 산출. hot률 = hot 세션 / 해당 날짜의 액티브 세션)
가장 중요한 것은 왼쪽 열로, 세션 중앙값의 cacheRead/output 비가 233x → 130x → 110x → 83x로 단조 감소하고 있다.
이것이 효과가 있다는 근거는, 같은 기간 동안 세션의 사용 방식이 '다수의 짧은 세션(중앙값 2개 리퀘스트)'에서 '소수의 긴 세션(중앙값 59개 리퀘스트)'으로 변했기 때문이다. cache read는 문맥 길이(context length)에 비례하여 매 리퀘스트마다 쌓이므로, 세션이 길어지면 이 비중은 보통 악화된다. 장시간화와 비율 저하가 동시에 일어나고 있는 것은, 임계값에서 /compact를 실행하여 live context를 낮추고 있는 효과와 일치한다.
악화된 지표도 솔직하게 적어둔다. hot률(≥80req 세션의 비율)은 30% → 44%로 상승했다. 세션을 집약시킨 부작용으로, 폐루프는 'hot해지면 끊어내는' 메커니즘이지 'hot해지지 않게 만드는' 메커니즘이 아니다. 이 부분은 설계대로지만, 기대와의 차이로서 명기해 둔다.
또 하나, '최악의 cacheRead/output 비'는 7/4에 891배로 과거 최악을 경신했으나, 실체는 출력이 6.5k 토큰밖에 되지 않는 문서 열람 세션이었다 (cache read의 절대량은 5.8M으로 작다). 비율만 보면 저출력 세션이 상위권을 독점하므로, 이 지표는 절대량과 함께 읽지 않으면 오해하기 쉽다.
compact 경계 레코드의 일간 카운트는 다음과 같았다: 1회 → 21회 → 7회 → 1회.
가동 다음 날인 7/3에는 훅(hook)이 시키는 대로 21회나 끊었다. 대역폭별 실측(100k 미만에서 끊어도 거의 무의미, 200k 초과 시 8할 감소)을 알게 된 후부터는 횟수가 줄었다. 횟수가 줄어든 후에도 중앙값 비는 계속 낮아지고 있으므로(110x → 83x), 횟수보다는 끊는 타이밍이 효과적일 가능성이 높다.
인과관계를 단정 짓지는 않는다. n=4일(그중 부분적 날 1일)·1사용자·워크로드 비통제 조건이다.
다만 '세션 내 직접 측정(중앙값 66% 감소)'과 '정규화 시계열의 단조 개선(233x→83x)'이라는 독립적인 두 계통이 같은 방향을 가리키고 있으며, 폐루프가 cache read의 누적을 깎아내고 있다는 설명이 현시점에서 가장 정합적이라고 생각한다.
생 데이터와 재현 절차는 리포지토리의 docs/verification-2026-07-04.md / docs/verification-2026-07-05.md에 두었다.
4일간의 실측을 통해 알 수 있는 운용의 요점을 정리한다.
압축 후의 플로어(floor)는 약 5064k 토큰이며, 그 이하로는 줄어들지 않는다. **플로어의 2배(약 100130k) 미만에서 끊으면 중앙값 기준 14%밖에 줄어들지 않는다**. 200k 초과에서 끊으면 8할 전후로 줄어든다.
「자주 /compact를 하는 것」은 무의미했다. 임계값(threshold)을 넘어 과열된 후, 태스크의 구분점에서 끊는다.
모니터링 지표:
- 세션 중앙값의 cacheRead/output 비율 (절대량과 함께 읽을 것) — 폐루프(closed-loop)의 효과가 가장 솔직하게 나타났다.
- main-thread share와 agent × model 내역 — 위임(delegation)이 실제로 일어나고 있는지는 로그를 통해서만 확인할 수 있다.
단독으로는 보지 않는 지표:
-
cacheRd% — 구조적으로 90% 전후에서 고정되어 거의 움직이지 않는다. 성공 지표가 될 수 없다.
-
worst 비율 — 출력이 적은 세션이 상위권을 독점한다 (891x의 실체는 출력이 6.5k인 열람 세션). 절대량과 함께 읽는 것이 필수적이다.
-
hot률 — 세션을 집약하면 악화된다. 폐루프 설계상 예상 범위 내의 움직임이다.
-
requestId로 중복 제거를 해야 한다 (그렇지 않으면 2~3배로 부풀려진다). -
subagents/하위의 별도 파일을 읽어야 한다 (그렇지 않으면 위임이 0인 것처럼 보인다). -
측정 당일의 버킷(bucket)을 확정값으로 삼지 않는다 (전체 일자로 마감하면 숫자가 변한다).
ccusage는 비용 및 사용량 리포트로서 강력하며, Claude HUD는 컨텍스트 사용량(context usage), 활성 도구(active tools), 실행 중인 에이전트(running agents), 할 일 진행 상황(todo progress) 등을 상시 표시할 수 있다.
둘 다 편리하며, 경쟁 관계라기보다 보완 관계에 가깝다.
다만, 이번에 만들고자 했던 것은 '시각화(visualization)'가 아니었다.
만들고자 했던 것은, 임계값을 넘는 순간 모델의 거동을 바꾸는 closed-loop였다.
시각화는 인간이 보는 것을 전제로 한다.
하지만 인간은 보지 않게 된다.
그렇기에 시각화뿐만 아니라, 모델 스스로에게 "지금이 끊을 때다"라고 알려준다.
그리고 모델이 태스크의 구분점에서 /compact나 새로운 세션을 제안하도록 한다.
조사한 범위 내에서는, UserPromptSubmit 훅(hook)에서 세션 상태(session health)를 확인하고, additionalContext를 통해 모델 자신의 행동을 바꾸는 도구는 발견되지 않았다. 반례가 있다면 알려달라.
이 플러그인의 승부처는 "가장 저렴한 개입점은 대시보드가 아니라 모델 자신의 행동이 아닐까"라는 점에 있다.
Claude Code의 플러그인은 강력하다. commands / hooks / agents / MCP servers 등을 추가할 수 있기 때문에, 잘 모르는 플러그인을 넣는 것은 당연히 무섭다고 생각한다. 나도 그렇게 생각한다.
따라서 이 플러그인이 무엇을 추가하는지 명시해 둔다.
session-health가 추가하는 것은 주로 두 가지다.
/session-health:usage-report- 로컬의
~/.claude/projects/하위에 있는 Claude Code transcript를 읽어 토큰 사용량(token usage)을 집계하는 슬래시 커맨드(slash command) - 집계 축은
project × session × subagent × model - 외부 전송은 하지 않음
- 로컬의
- 로컬의
UserPromptSubmit훅(hook)- 프롬프트 전송 시, 현재 세션 상태를 로컬 transcript로부터 읽음
- 임계값을 초과한 경우에만 짧은
additionalContext를 모델에 주입함 - 목적은 "다음 구분점에서 /compact 또는 새 세션을 제안하라"고 모델에게 알리는 것
이 플러그인은 다음을 추가하지 않는다.
- MCP server
- 외부 API 연동
- 상주 데몬(daemon)
- 네트워크 전송
- Python 표준 라이브러리 이외의 의존성
불안하다면 설치 전에 리포지토리 내의 다음 파일들을 확인해 주길 바란다.
.claude-plugin/plugin.json
.claude-plugin/marketplace.json
hooks/hooks.json
commands/usage-report.md
scripts/session_health.py
scripts/usage_report.py
특히 살펴봐야 할 것은 hooks/hooks.json이다.
여기에 Claude Code가 어느 타이밍에 무엇을 실행하는지가 적혀 있다.
"개인 리포지토리의 플러그인을 경계하는 것"은 올바른 감각이라고 생각한다.
그럼에도 불구하고, 내용을 확인하고 판단해 주길 바란다.
도입은 두 줄이면 된다.
/plugin marketplace add House-lovers7/claude-code-session-health
/plugin install session-health@house-lovers7
훅(Hook)과 /session-health:usage-report는
즉시 사용할 수 있다.
/session-health:usage-report에서는
다음 4가지 축으로 토큰 내역을 확인할 수 있다.
project × session × subagent × model
statusline과 Stop 알림의 연결 방식은 README에 작성했다.
모든 과정은 로컬에서 완결된다.
외부로 전송하지 않는다.
임계값(Threshold)은 환경 변수로 조정할 수 있다.
인과관계의 증명은 아니다. n=4일·1사용자·워크로드 비통제 상태의 관측이며, 현재 시점의 주장은 독립된 2개 계통의 정합성까지이다. 7일 이상의 시계열 데이터가 쌓이면 제3보를 발표하겠다. -
hot 비율 처리는 미결 상태이다. 세션 집약(Aggregation)의 부작용으로서 허용할 것인지, 지표를 재정의할 것인지(예: hot 상태가 된 후 종료할 때까지의 체류 요청 수)는 향후 과제이다. -
transcript의 내부 사양에 의존한다. JSONL 레코드 형태나 subagents/ 배치는 비공개 사양이며, Claude Code의 향후 업데이트에 따라 집계 측의 수정이 필요할 가능성이 있다. - 임계값은 나의 워크로드에 맞춘 기본값이며, 환경 변수로 조정할 수 있다.
토큰 최적화의 주전장은 output이 아니라
cache read, 즉 **비대해진 세션의 재독(Re-reading)**이었다. -
집계 시
requestId 중복 제거, subagents/ 디렉토리, 부분 일자 버킷(Partial-day bucket)이라는 세 가지 함정에 빠지면 오진하게 된다. -
서브 에이전트(Sub-agent)는 단순히 사용하는 것에 그치지 않고,
어떤 agent가 어떤 model로 동작했는지까지 확인해야 한다. -
시각화만으로는 행동이 바뀌기 어렵다. 임계값을 초과하면,
모델 스스로에게 전환 시점을 제안하게 하는 것이 이번에 가장 효과적이었다. -
/compact는 세션 내의 live context를 중앙값 기준 66% 축소한다. 단, 종료(Cut)는 200k를 초과한 시점부터 수행한다. -
날짜를 넘나드는 경향성 또한, 정규화된 4일 시계열에서
**세션 중앙값의 cacheRead/output 비율이 233x → 83x로 단조 개선(Monotonic improvement)**되었다. 반면 cacheRd%와 hot 비율은 변하지 않았으며, 어떤 지표가 성공 지표가 될 수 있는가 자체를 실측해낸 것이 성과였다.
동일한 구조(탐지 → 모델로의 주입 → 행동 변경)는 위임의 철저한 실행, 보안 규약의 재확인, 거대 로그의 반입 억제 등 다른 '지켜지지 않는 규칙들'에도 응용할 수 있을 것이다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기