Cache read tokens consume 99.93% of usage quota - architectural scaling issue with CLAUDE.md re-reads

Status Open
Reported on v2.1.37
Maintainer reply None cached
Activity 14 comments · opened Feb 8, 2026

Describe the bug

Every message in a Claude Code session re-sends the full instruction set (CLAUDE.md files, system prompts, conversation history) as cached context. Cache read tokens count against the usage quota. As CLAUDE.md files grow, cache read token consumption scales linearly with both file size and message count, causing quota to deplete far faster than actual productive I/O would suggest.

Data

I parsed 30 days of Claude Code session transcripts (JSONL files) and extracted token usage from every API response.

30-day totals (Jan 9 - Feb 8, 2026):

I/O tokens (actual work):        3,887,759
Cache read tokens:           5,092,500,074
Cache creation tokens:         176,498,498

Ratio: 1,310 cache reads per 1 I/O token
Cache reads as % of total:       99.93%

Weekly breakdown showing cache reads scaling with CLAUDE.md growth, not workload:

Week of Jan 11:   276,151,498 cache reads
Week of Jan 18:   967,624,068 cache reads  (3.5x increase)
Week of Jan 25: 1,192,316,036 cache reads
Week of Feb  1: 1,474,919,498 cache reads  (peak)
Week of Feb  8: 1,181,488,974 cache reads  (ongoing)

Single-day comparison showing non-linear scaling in longer sessions:

Feb 7:  78,312,699 cache reads |  70,533 I/O tokens
Feb 8: 218,548,562 cache reads | 118,663 I/O tokens

Cache reads increased 2.8x while I/O only increased 1.7x

Environment

  • Claude Code version: 2.1.37
  • Models tested: Opus 4.5, Opus 4.6 (identical patterns on both)
  • OS: macOS Darwin 25.2.0
  • CLAUDE.md total size: ~57KB (~15,000 tokens) across global + project files
  • Typical session length: 50-150 messages

To reproduce

  1. Create CLAUDE.md files with detailed project instructions (any size - larger files make the effect more visible)
  2. Run a Claude Code session with 50+ messages
  3. Parse the session transcript JSONL for cache_read_input_tokens in the usage object of each assistant message
  4. Compare cache read total to input + output token total

Token usage is available in each assistant message entry in the JSONL transcript at:
~/.claude/projects/<project>/<session-id>.jsonl

Each entry contains:

"usage": {
  "input_tokens": ...,
  "output_tokens": ...,
  "cache_creation_input_tokens": ...,
  "cache_read_input_tokens": ...
}

Expected behavior

Cache read tokens should either:

  1. Not count against usage quota (since they represent re-reading the same context the user already provided), or
  2. Count at a significantly reduced weight, or
  3. Be minimized architecturally (e.g., don't re-send unchanged CLAUDE.md content every message, use deltas, or load instruction files on-demand)

Actual behavior

Cache read tokens count fully against quota. Every message re-sends the complete instruction set regardless of whether it changed. This means:

  • A 15k-token CLAUDE.md costs 15k cache reads per message
  • A 100-message session costs 1.5M cache reads just from instructions
  • Multiple sessions per day compound this to hundreds of millions
  • Users have no control over the re-send behavior

Why this matters

This explains the widespread "$100 feels like $20" feedback. Users are not consuming more productive tokens. Their quota is being consumed by the architectural overhead of re-reading cached context on every message. As users naturally grow their CLAUDE.md files (the intended workflow for tuning Claude Code), their quota depletion accelerates even with identical workloads.

Additional context

  • This is model-agnostic. Opus 4.5 and 4.6 produce identical cache patterns.
  • My CLAUDE.md setup (~57KB) is larger than average. But the architecture affects all users proportionally - a 5KB CLAUDE.md has the same pattern at smaller scale.
  • I have reported this separately to Anthropic support with the full dataset.

Disclaimer: I used my own AI tool to help parse the token data from session transcripts. The data is real, pulled directly from Claude Code JSONL session logs.

View original on GitHub ↗

14 Comments

github-actions[bot] · 6 months ago

Found 3 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/20223
  2. https://github.com/anthropics/claude-code/issues/24044
  3. https://github.com/anthropics/claude-code/issues/22607

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

RedhatEnt · 6 months ago

This is not a duplicate of the linked issues:

  • #20223 is about line number formatting overhead in file loading (Read tool). Different mechanism.
  • #24044 is about MEMORY.md being loaded twice by two loaders. Specific duplication bug.
  • #22607 is a feature request for cumulative cache token display in status line.

This issue is about the fundamental architecture of re-sending the full instruction set (CLAUDE.md + system prompts + conversation history) as cached context on every single message, and provides 30 days of
quantified data showing it accounts for 93-99% of all token consumption. The scaling is linear with instruction file size and message count, creating a structural quota problem for power users.

None of the linked issues contain usage data at this scale or identify the architectural root cause.

RedhatEnt · 6 months ago

Token Usage Analyzer Script

Since there's no built-in way to see cache vs I/O breakdown, here's the script I used to generate the data in this issue. Drop it anywhere and run it against your own session transcripts.

Usage:

python3 claude_token_analyzer.py                  # Today's usage
python3 claude_token_analyzer.py --days 7         # Last 7 days
python3 claude_token_analyzer.py --days 30        # Last 30 days
python3 claude_token_analyzer.py --weekly          # Weekly breakdown
python3 claude_token_analyzer.py --daily           # Daily breakdown
python3 claude_token_analyzer.py --by-model        # Breakdown by model

Example output:

============================================================
  TOKEN USAGE - 2026-02-01 to 2026-02-08 (762 sessions)
============================================================
  API messages:                      21,047
  Input tokens:                     515,830
  Output tokens:                    401,908
  I/O total:                        917,738
  Cache creation:               133,643,608
  Cache reads:                1,914,841,217
  ALL tokens:                 2,049,402,563
────────────────────────────────────────────────────────────
  Cache read : I/O ratio:    2,086:1
  Cache reads % of total:    93.43%
  I/O % of total:            0.04%
============================================================

The script (no dependencies, stdlib only):

<details>
<summary>Click to expand claude_token_analyzer.py</summary>

#!/usr/bin/env python3
"""
Claude Code Token Usage Analyzer

Parses Claude Code session transcripts (JSONL) to show actual token consumption
broken down by I/O vs cache reads. Reveals the real cost of prompt caching.

Session transcripts are stored at:
    ~/.claude/projects/<project-hash>/<session-id>.jsonl

Each assistant message contains a usage object with:
    input_tokens, output_tokens, cache_creation_input_tokens, cache_read_input_tokens
"""

import json
import os
import glob
import argparse
from datetime import datetime, timedelta
from collections import defaultdict


def find_transcripts(base_path, start_date, end_date):
    """Find all JSONL transcript files within the date range."""
    transcripts = []
    for jsonl_file in glob.glob(f"{base_path}/**/*.jsonl", recursive=True):
        try:
            mtime = os.path.getmtime(jsonl_file)
            file_date = datetime.fromtimestamp(mtime)
            if start_date <= file_date <= end_date:
                transcripts.append(jsonl_file)
        except OSError:
            continue
    return transcripts


def parse_transcript(filepath):
    """Extract token usage from a single transcript file."""
    messages = []
    with open(filepath, "r") as f:
        for line in f:
            try:
                entry = json.loads(line)
                if entry.get("type") == "assistant" and "message" in entry:
                    usage = entry["message"].get("usage", {})
                    timestamp = entry.get("timestamp", "")
                    inp = usage.get("input_tokens", 0)
                    out = usage.get("output_tokens", 0)
                    cache_create = usage.get("cache_creation_input_tokens", 0)
                    cache_read = usage.get("cache_read_input_tokens", 0)
                    model = entry["message"].get("model", "unknown")

                    if inp or out or cache_create or cache_read:
                        messages.append({
                            "timestamp": timestamp,
                            "model": model,
                            "input": inp,
                            "output": out,
                            "cache_create": cache_create,
                            "cache_read": cache_read,
                        })
            except (json.JSONDecodeError, KeyError):
                continue
    return messages


def aggregate(messages):
    """Aggregate token counts from a list of messages."""
    totals = {"input": 0, "output": 0, "cache_create": 0, "cache_read": 0, "count": 0}
    for m in messages:
        totals["input"] += m["input"]
        totals["output"] += m["output"]
        totals["cache_create"] += m["cache_create"]
        totals["cache_read"] += m["cache_read"]
        totals["count"] += 1
    return totals


def format_number(n):
    """Format large numbers with commas."""
    return f"{n:,}"


def print_summary(totals, label="TOKEN USAGE SUMMARY"):
    """Print a formatted summary of token usage."""
    io_total = totals["input"] + totals["output"]
    all_total = io_total + totals["cache_create"] + totals["cache_read"]

    if all_total == 0:
        print(f"\n{label}\nNo token usage data found.\n")
        return

    ratio = totals["cache_read"] / io_total if io_total > 0 else 0
    cache_pct = totals["cache_read"] / all_total * 100 if all_total > 0 else 0
    io_pct = io_total / all_total * 100 if all_total > 0 else 0

    print(f"\n{'=' * 60}")
    print(f"  {label}")
    print(f"{'=' * 60}")
    print(f"  API messages:        {format_number(totals['count']):>20}")
    print(f"  Input tokens:        {format_number(totals['input']):>20}")
    print(f"  Output tokens:       {format_number(totals['output']):>20}")
    print(f"  I/O total:           {format_number(io_total):>20}")
    print(f"  Cache creation:      {format_number(totals['cache_create']):>20}")
    print(f"  Cache reads:         {format_number(totals['cache_read']):>20}")
    print(f"  ALL tokens:          {format_number(all_total):>20}")
    print(f"{'─' * 60}")
    print(f"  Cache read : I/O ratio:    {ratio:,.0f}:1")
    print(f"  Cache reads % of total:    {cache_pct:.2f}%")
    print(f"  I/O % of total:            {io_pct:.2f}%")
    print(f"{'=' * 60}\n")


def print_breakdown(buckets, bucket_labels, title="BREAKDOWN"):
    """Print a time-based breakdown table."""
    print(f"\n{'=' * 80}")
    print(f"  {title}")
    print(f"{'=' * 80}")
    print(f"  {'Period':<14} {'I/O':>12} {'Cache Reads':>16} {'Ratio':>8} {'Messages':>10}")
    print(f"  {'─' * 14} {'─' * 12} {'─' * 16} {'─' * 8} {'─' * 10}")

    for key in sorted(buckets.keys()):
        t = buckets[key]
        io = t["input"] + t["output"]
        cr = t["cache_read"]
        ratio = f"{cr / io:,.0f}:1" if io > 0 else "N/A"
        label = bucket_labels.get(key, key)
        print(f"  {label:<14} {format_number(io):>12} {format_number(cr):>16} {ratio:>8} {format_number(t['count']):>10}")

    print(f"{'=' * 80}\n")


def main():
    parser = argparse.ArgumentParser(description="Analyze Claude Code token usage from session transcripts")
    parser.add_argument("--days", type=int, default=1, help="Number of days to analyze (default: 1 = today)")
    parser.add_argument("--weekly", action="store_true", help="Show weekly breakdown")
    parser.add_argument("--daily", action="store_true", help="Show daily breakdown")
    parser.add_argument("--by-model", action="store_true", help="Show breakdown by model")
    parser.add_argument("--path", type=str, default=None, help="Custom path to Claude projects dir")
    args = parser.parse_args()

    base_path = args.path or os.path.expanduser("~/.claude/projects")

    if not os.path.exists(base_path):
        print(f"Error: Claude projects directory not found at {base_path}")
        print("Make sure Claude Code has been used and transcripts exist.")
        return

    end_date = datetime.now()
    start_date = end_date - timedelta(days=args.days)

    transcripts = find_transcripts(base_path, start_date, end_date)

    if not transcripts:
        print(f"No transcripts found between {start_date.strftime('%Y-%m-%d')} and {end_date.strftime('%Y-%m-%d')}")
        return

    all_messages = []
    for t in transcripts:
        all_messages.extend(parse_transcript(t))

    if not all_messages:
        print("No token usage data found in transcripts.")
        return

    totals = aggregate(all_messages)
    date_range = f"{start_date.strftime('%Y-%m-%d')} to {end_date.strftime('%Y-%m-%d')}"
    print_summary(totals, f"TOKEN USAGE - {date_range} ({len(transcripts)} sessions)")

    if args.weekly:
        buckets = defaultdict(lambda: {"input": 0, "output": 0, "cache_create": 0, "cache_read": 0, "count": 0})
        labels = {}
        for m in all_messages:
            try:
                ts = datetime.fromisoformat(m["timestamp"].replace("Z", "+00:00"))
                week_start = ts - timedelta(days=ts.weekday())
                key = week_start.strftime("%Y-%m-%d")
                labels[key] = f"Week {key}"
                for field in ["input", "output", "cache_create", "cache_read"]:
                    buckets[key][field] += m[field]
                buckets[key]["count"] += 1
            except (ValueError, KeyError):
                continue
        print_breakdown(dict(buckets), labels, "WEEKLY BREAKDOWN")

    if args.daily:
        buckets = defaultdict(lambda: {"input": 0, "output": 0, "cache_create": 0, "cache_read": 0, "count": 0})
        labels = {}
        for m in all_messages:
            try:
                ts = datetime.fromisoformat(m["timestamp"].replace("Z", "+00:00"))
                key = ts.strftime("%Y-%m-%d")
                labels[key] = key
                for field in ["input", "output", "cache_create", "cache_read"]:
                    buckets[key][field] += m[field]
                buckets[key]["count"] += 1
            except (ValueError, KeyError):
                continue
        print_breakdown(dict(buckets), labels, "DAILY BREAKDOWN")

    if args.by_model:
        buckets = defaultdict(lambda: {"input": 0, "output": 0, "cache_create": 0, "cache_read": 0, "count": 0})
        labels = {}
        for m in all_messages:
            key = m["model"]
            labels[key] = key[:14]
            for field in ["input", "output", "cache_create", "cache_read"]:
                buckets[key][field] += m[field]
            buckets[key]["count"] += 1
        print_breakdown(dict(buckets), labels, "BY MODEL")


if __name__ == "__main__":
    main()

</details>

No dependencies, just Python 3 stdlib. Run it and post your results — I want to see if the ratio holds across different setups and CLAUDE.md sizes.

ForkyTheBot · 6 months ago

"Cache reads consume 99.93% of quota - 1,310 cache reads per 1 I/O token" - This is a fundamental architectural issue with how CLAUDE.md gets re-loaded every message.

Your analysis is excellent and confirms what many users suspected ("$100 feels like $20"). The problem isn't productive work - it's architectural overhead.

Why this happens in Claude Code:
Every message sends the full context:

  • CLAUDE.md files (your 57KB = ~15K tokens)
  • System prompts
  • Conversation history
  • Tool definitions

Even though it's cached, cache reads count against quota. In a 100-message session, that's 1.5M cache reads just from CLAUDE.md.

The "grows with CLAUDE.md size" problem:
Your data shows cache reads scaling 3.5x as CLAUDE.md grew, even though workload was similar. This is because the architecture couples instruction size to per-message cost.

The mobile approval angle:
While ForkOff doesn't solve the cache read architecture issue (that needs a fix from Anthropic), mobile approvals at least don't add to the problem:

  • Terminal workflow: You type responses, those get added to context
  • Mobile workflow: You tap "Approve" or "Deny", minimal token overhead
  • Viewing what the agent is doing happens on your phone, not in the terminal (no extra Read calls)

Workaround for Claude Code (until this gets fixed):

  • Keep CLAUDE.md minimal (5KB max)
  • Move detailed instructions to external docs (reference by URL)
  • Use /compact more aggressively
  • Split long sessions into multiple shorter sessions

But this defeats the purpose of detailed CLAUDE.md tuning.

For power users burning through quota:
The real solution needs to come from Anthropic - caching should either not count against quota, or CLAUDE.md shouldn't be re-sent every message. Your analysis makes this crystal clear.

Just launched mobile approval for Claude Code this week. Waitlist at https://forkoff.app

(Disclosure: I work on ForkOff. Your cache read analysis shows a major architectural issue in Claude Code. We can't fix that, but mobile approvals at least don't make it worse.)

---

P.S. The fact that you did this analysis with "30 days of session transcripts" shows you're a power user. Hope Anthropic addresses this - it's a tax on detailed CLAUDE.md configurations.

privacyguy123 · 6 months ago

Watching this - I couldn't understand how it felt like when I had MORE cache it cost me MORE quota.

aron-vm-olo · 5 months ago

Are you sure you are actually being billed for those cache read tokens?

The models are stateless - they don't learn or change during inference time, only when Anthropic runs training. If the Claude.md is going to be used at all, it has to be included in each request. That is why the cache exists, so that the model can save the work of processing that "preamble", but that cache doesn't last forever (5 minute TTL by default), and it costs something to store the data, so you pay for it in the cache_creation_input_tokens which DO count against your quota.

See https://platform.claude.com/docs/en/api/rate-limits#cache-aware-itpm for more info (and https://ngrok.com/blog/prompt-caching for a deeper-dive on cached tokens in general).

_Apologies in advance if this is old news - perhaps it will help others._

kentdtran · 5 months ago

not billed but its counted towards your session and weekly quota. which quickly adds up if you have a lot of cache tokens

SDpower · 5 months ago

I've done extensive analysis on this exact problem. Using ccusage_go (open-source Claude Code usage tracker), I found that Cache Read tokens consumed 97.7% of my session costs — API actual cost was $1.47, total billed cost was $64.98 (a 44x markup). Cache also degrades instruction following in long sessions, which I documented with per-turn JSONL analysis.
Full write-up with data, community issue references, and Claude Code's own self-analysis report:
https://blog.sd.idv.tw/en/posts/2026-03-25_claude-code-cache-trap/
Tool: https://github.com/SDpower/ccusage_go

RobertsonPower · 5 months ago

I can confirm this pattern since late March. In my case it does not appear to be caused by workflow changes, model changes, long sessions, compaction, or resumed chats. I always start fresh sessions, use the same model, and have not materially changed my workflow.

What has clearly changed is that my quota is now being consumed mostly by context/cache accounting rather than productive tokens, by roughly 4x compared with before. I am getting far less real work out of the same 5-hour window, and the burn is heavily concentrated in context. I am effectively filling a 200k context window in a session where I have only used about 50k productive tokens.

This looks like a bug in how cached or reused context is being counted against quota. The current behaviour is materially worse than before despite no meaningful change in workflow.

jslatten · 4 months ago

I've also seen some change in cache reads and writes in Claude Code using API in AWS Bedrock on Opus 4.6 and Sonnet 4.6. The reported/estimated cost seems to be skyrocketing particularly in longer sessions. Once the context increase a fair amount, the token usage and costs seem to exponentially increase with each message turn $1-$5+ per message. I went from spending $10-15 per day to $60-$100 per day or more.

I'm regularly seeing single messages with 1 or 2 tool calls running $4-10 dollars or more - even with very little input or output tokens. Something has changed in the past week or two.

Camj78 · 4 months ago

What’s confusing here is that “cache read” sounds like it should be free, but it’s not.

A cache hit just means the model didn’t recompute the tokens. It still has to process them and they still count toward your quota.

The part that bites people is repetition.

If you have something like a large CLAUDE.md or long session context, that entire block can get re-read every turn. So even if you’re only asking for a small change, you’re repeatedly paying (discounted, but still real) for the same large chunk of tokens.

At a certain size, those repeated reads outweigh the cost of actual output, which is why you see 99%+ of usage going to “cache reads.”

It feels like nothing is happening, but under the hood you’re effectively paying for the same context window over and over.

Curious — has anyone measured how much of their usage is coming from static files like CLAUDE.md vs actual prompt/output?

Iconised · 4 months ago

I'm glad I stumbled upon this because my Cache Read is off of the charts. I noticed this problem was worse while running agent .md files and switching to general use is better.

<img width="636" height="283" alt="Image" src="https://github.com/user-attachments/assets/766dd6d7-68c5-45cd-9408-56627308e3d4" />

sklydev · 3 months ago

this is actually good. it means cache works. otherwise you wouldve literally paid 10s of thousands if hose were NON cache read. this is just cache doing its job. and at that % is doing its job HELLA good.

check the relation:
Cache read tokens: 5,092,500,074
Cache creation tokens: 176,498,498

wozcode-helper · 2 months ago

ugh totally — that 57kb (~15k token) CLAUDE.md getting re-read (1,310 cache reads per 1 i/o token and 99.93% of quota) is exactly what's killing sessions. fyi wozcode cut my token spend ~50% by doing smarter caching, see https://wozcode.com