{"slug": "i-counted-30-days-of-my-claude-code-tokens-98-were-cache-reads", "title": "I Counted 30 Days of My Claude Code Tokens. 98% Were Cache Reads.", "summary": "A developer who built a menu bar app to tally Claude Code usage found 4.9 billion tokens consumed over 30 days, of which 98.1% were cache reads and just 0.37% were output tokens written by the model, according to the developer's own count of the JSONL session files Claude Code writes under ~/.claude/projects/. The developer reported that a naive script summing every line carrying a usage object overcounts output tokens 2.47 times and cache reads 2.13 times, because a single reply can span multiple lines and appear in two files when a session is resumed or forked; the fix keys each reply by message ID and request ID and keeps the largest value per counter. The developer also warned that Claude Code deletes session files older than cleanupPeriodDays, 30 days by default, so users must raise that setting in ~/.claude/settings.json before older history is lost.", "body_md": "I pay a flat monthly fee for Claude, so for months I never thought about tokens. Then I built a small menu bar app that counts them, pointed it at my own machine, and got a number I didn’t believe: 4.9 billion tokens in 30 days.\n\nThat number doesn’t come from a dashboard. It comes from plain JSON files in my home folder, written by Claude Code after every reply. And the part that surprised me most wasn’t the size. It was the mix. 98.1% of those tokens were cache reads: the conversation so far, sent again with every request and served from Anthropic’s prompt cache. The text Claude actually wrote back to me was 0.37%.\n\nThis article is what I learned reading those files: how they are laid out, how to count them correctly with 25 lines of Python, what my month actually looks like, and where the 5-hour and weekly plan limits live. That last one is a separate question with a separate answer, and it took me longer than it should have to figure out.\n\nEvery session is a JSONL file: one JSON object per line, appended as the session runs. They live under ~/.claude/projects/, in a folder per project, named after the project's path:\n\n```\n~/.claude/projects/  -Users-you-code-api/    3f2c….jsonl          # one session    3f2c…/subagents/     # its subagents' sessions\n```\n\nI run two accounts, one per CLAUDE_CONFIG_DIR, and each config folder has its own projects/. If you do the same, remember to count both.\n\nA line is one event: your prompt, a tool result, a summary, or a reply from the model. Reply lines carry a usage object from the API, with four counters:\n\nHere is the thing that took me a while to really feel: each request sends the whole conversation again. Claude Code’s own cost docs put it plainly. A one-line question in a session that has been open all day “still draws usage for the whole conversation.” Caching is what makes that resend cheap, and the usage object tells you exactly how cheap.\n\nOne warning before you count anything. Claude Code deletes session files older than cleanupPeriodDays, which is 30 days by default. If you want more than a month of history, raise it in ~/.claude/settings.json now, before you need it. You can't count what's already gone.\n\nThe obvious script reads every line with usage and adds the numbers up. On my logs, that overcounts output tokens 2.47 times and cache reads 2.13 times. Your total would look more than twice as scary as it really is.\n\nThe reason: one reply is often written as several lines. A reply with thinking, text and a tool call can land as one line per content block, and each line carries the same usage. My 30 days had 51,429 lines with usage, but only 24,434 distinct replies. A reply can also show up in two files when a session is resumed or forked.\n\nThe fix is to key each reply by its message ID and request ID, and keep the largest value per counter across its copies:\n\n``` python\nimport json, glob, osfrom collections import Counterroot = os.path.expanduser(\"~/.claude/projects\")replies = {}  # (message id, request id) -> usagefor path in glob.glob(f\"{root}/**/*.jsonl\", recursive=True):    for line in open(path, errors=\"ignore\"):        if '\"usage\"' not in line:            continue  # skip prompts, tool results, summaries        row = json.loads(line)        msg = row.get(\"message\") or {}        if row.get(\"type\") != \"assistant\" or \"usage\" not in msg:            continue        key = (msg.get(\"id\"), row.get(\"requestId\"))        seen = replies.setdefault(key, Counter())        for field, n in msg[\"usage\"].items():            if isinstance(n, int):                seen[field] = max(seen[field], n)  # copies of one reply: keep the largesttotal = sum(replies.values(), Counter())print(len(replies), \"replies\")for field in (\"input_tokens\", \"cache_creation_input_tokens\",              \"cache_read_input_tokens\", \"output_tokens\"):    print(f\"{field:30} {total[field]:>15,}\")\n```\n\nThe string check before json.loads matters on a big folder. Most lines are prompts and tool output, and skipping them unparsed keeps the run to seconds. My folder is 1.6 GB and it takes about 9 seconds.\n\nWhy the largest value and not the last one? Copies of the same reply should agree, and taking the largest is safe when they do. When they don’t, the largest is the complete count.\n\nI ran the count over both accounts for 2026–09–26 to 2026–10–26, then priced each reply at Anthropic’s API list prices for its model. I’m on a subscription, so this is not what I paid. It’s what the same tokens would cost on the API, which is the honest way to compare months or models.\n\nYes, most of that is Opus. I know. Three things stood out to me.\n\n**Caching is the whole story.** Cache reads are billed at a tenth of the input price or less. Priced as plain input, my 4.8 billion cache reads would cost $18,083 on their own, ten times the whole month. So the expensive event isn’t a long answer. It’s anything that breaks the cache: changing the system prompt mid-session, or coming back after the cache has expired.\n\n**Output is tiny in tokens and big in dollars.** 0.37% of the tokens, 19% of the value, because output costs five times input per token.\n\n**Uncached input is basically zero.** 49 thousand tokens in a whole month. Nearly every input token was either written to the cache or read from it.\n\nIf you want to use less, these numbers point at context size, not at shorter answers. Every turn re-reads everything before it. That makes /clear between unrelated tasks worth more than any clever prompt, and it's the habit I'm taking away from this.\n\nToken counts don’t tell you how close you are to the 5-hour or weekly limit on a Pro or Max plan. That figure belongs to the server, and Claude Code exposes it in two places I know of.\n\n**1. The status line input.** Claude Code runs your status line command with a JSON object on stdin. It includes rate_limits.five_hour and rate_limits.seven_day, each with used_percentage and resets_at (Unix seconds). It's fresh with every reply.\n\n**2. A cache in** **.claude.json.** Claude Code also saves a reading of each window there when it checks your account. It's convenient because it's a file you can read any time, but it only updates now and then. On my Mac it was 8 and 21 hours old while Claude Code was running.\n\nA small status line script can save the fresh figures somewhere you can read them later:\n\n``` bash\n#!/bin/bashinput=$(cat)limits=$(echo \"$input\" | jq -c '.rate_limits // empty')  # absent on API billing[ -n \"$limits\" ] && echo \"$limits\" > ~/.claude/limits.jsonecho \"$input\" | jq -r '\"5h: \\(.rate_limits.five_hour.used_percentage // \"?\")%  week: \\(.rate_limits.seven_day.used_percentage // \"?\")%\"'\n```\n\nThe app I mentioned is OpusBar, a macOS menu bar app that reads these files and shows them to me. A pixel cat changes pose for each Claude Code or Codex session (working, thinking, needs you, done, error, idle), because I’d rather glance at a cat than at another wall of percentages. A Usage tab shows tokens and API value over 24 hours, 7, 30 and 90 days, plus each account’s 5-hour and weekly limits, with warnings at 80% and 95%. It’s free and open source (Apache-2.0), and I’m the only author. I’m not affiliated with Anthropic or OpenAI.\n\nIt takes its data from three places, each a different kind of source:\n\nOne rule sits behind all three: no network calls and no sign-in tokens. Some tools read your OAuth token from the Keychain and call the usage endpoint. That works, but it means a menu bar app holding your credentials, and I wasn’t comfortable with that for something whose whole job is to sit in the corner and look cute.\n\nThis was the first thing I had to get right, before a single number reached the screen. The key OpusBar uses is message ID plus request ID, with copies merged by the largest value per counter. It also had to hold for Codex, which logs a running total per session. There, two copies of the same total must count once, not twice.\n\nMy first version read limits from the Claude desktop app’s own usage file. On my Mac that file didn’t exist, so the first time I opened my own Usage tab, no Claude limits showed at all. Not a great feeling when you’ve just built the thing.\n\nThe second version read the .claude.json cache. The cards looked right. Then I checked the timestamp: 21 hours old. A warning at 80% is useless if it's describing yesterday. Moving to the status line input fixed it, with one cost: the figure is only as fresh as the last reply on this Mac. Usage from your phone shows up after your next reply here.\n\nOpusBar prices tokens from a table shipped inside the app, with the date it was last checked. A model the table doesn’t know is counted as “unpriced,” never guessed. While running the numbers for this very article, 420 of my replies came back unpriced: Haiku 5.5, released after the table’s date. So my own app had a gap I only found by writing about it. The table needs an update, and I’m glad it said “unpriced” instead of quietly borrowing a sibling model’s price and hiding the problem.\n\nSynchronous hooks keep order but add 10 to 20 ms to every tool call. OpusBar registers async hooks, which never slow Claude down but can arrive out of order. Each event carries the time its hook started, and the app drops any event older than the last one it applied for that session. The one exception is session end, which always removes the session, even if it arrives late.\n\nIf the app is closed, the hook still runs on every tool call. So it connects with a 200 ms deadline, never prints, and always exits 0. Quitting OpusBar is invisible to Claude Code. I think anything you put in a hook should pass the same test: what happens to the agent when your side is down?\n\nNo hook fires when you answer a permission prompt. So “needs you” can’t clear on the answer. It clears on the next tool call, prompt or stop event. After a denied prompt, a session can show “needs you” until Claude does something else. I kept the session logic as a pure function from (state, event, time) to state, so every one of these odd cases is a fast unit test instead of a bug report.\n\nYou don’t need OpusBar to see your own numbers. The script above runs on any Mac or Linux machine with Claude Code. Set cleanupPeriodDays first if you want more than a month of history. Then run it and see what your mix looks like. I'd genuinely like to know if anyone's is less than 90% cache reads.\n\nIf you want the menu bar version (macOS 14 or later):\n\n```\ncurl -fsSL https://raw.githubusercontent.com/kushalBanda/OpusBar/main/scripts/install.sh | bash\n```\n\nThe source is on GitHub at kushalBanda/OpusBar. Read the install script first; it’s short.\n\nResources\n\nWhat did your own count show? If you found this useful, **give it a clap and drop a comment**. I’d love to hear your numbers 🙌\n\nFollow me here on [Medium](https://medium.com/@kushalbanda) for more Claude Code deep dives, or connect on [LinkedIn](https://www.linkedin.com/in/kushalbanda/).\n\n[I Counted 30 Days of My Claude Code Tokens. 98% Were Cache Reads.](https://pub.towardsai.net/i-counted-30-days-of-my-claude-code-tokens-98-were-cache-reads-f8463af4515d) was originally published in [Towards AI](https://pub.towardsai.net) on Medium, where people are continuing the conversation by highlighting and responding to this story.", "url": "https://wpnews.pro/news/i-counted-30-days-of-my-claude-code-tokens-98-were-cache-reads", "canonical_source": "https://pub.towardsai.net/i-counted-30-days-of-my-claude-code-tokens-98-were-cache-reads-f8463af4515d?source=rss----98111c9905da---4", "published_at": "2026-11-05 05:09:28+00:00", "updated_at": "2026-11-05 05:25:10.480591+00:00", "lang": "en", "topics": ["ai-tools", "large-language-models", "ai-products"], "entities": ["Claude Code", "Anthropic", "Claude"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/i-counted-30-days-of-my-claude-code-tokens-98-were-cache-reads", "markdown": "https://wpnews.pro/news/i-counted-30-days-of-my-claude-code-tokens-98-were-cache-reads.md", "text": "https://wpnews.pro/news/i-counted-30-days-of-my-claude-code-tokens-98-were-cache-reads.txt", "jsonld": "https://wpnews.pro/news/i-counted-30-days-of-my-claude-code-tokens-98-were-cache-reads.jsonld"}}