[BUG] Same-day 17x variance in tokens charged per weekly quota point (Max 20x, 2.1.221)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
The weekly usage counter charges very different amounts of quota for the same measured work,
within a single day and a single weekly window.
Aug 5, local time (UTC-10), Max 20x. Counts are API calls, not transcript lines (see Method):
07:59-09:00 : 58 calls, 3.0M weighted tokens -> counter 33% to 59% (+26 points)
09:00-17:00 : 583 calls, 29.5M weighted tokens -> counter 61% to 76% (+15 points)
Ten times the work in the afternoon, for 42% fewer quota points. A quota point cost 0.12M in
the morning and 1.97M in the afternoon: a factor of 17, on the same plan, the same weekly
window, the same machine and the same build (2.1.221), with no configuration change between.
The morning is worse than that table suggests. Nothing at all was sent between 00:00 and 07:59.
The first call of the day ran at 07:59, and the session the jump landed in
(878f0610-6ec0-4678-9331-2079ae88dd2b) opened at 08:05. Only 1.0M weighted tokens had been sent
all morning before that session opened, and that session totals 25 calls and 0.7M over its whole
24-minute life. Most of the 3.0M above was spent AFTER the counter had already moved.
The day before shows the same shape in reverse:
Tue Aug 4, 17:00-23:00 : 321 calls, 15.8M weighted -> +3 points (5.27M per point)
Wed Aug 5, 07:59-09:00 : 58 calls, 3.0M weighted -> +26 points (0.12M per point)
The larger evening cost 3 points; the smaller morning cost 26. That is a factor of 45 between
two windows fourteen hours apart, inside one weekly window.
And this morning, Aug 6, gives a fourth reading in the same slot on consecutive days:
Wed Aug 5, 07:59-09:00 : 58 calls, 3.0M weighted -> +26 points (0.12M per point)
Thu Aug 6, 07:03-08:50 : 277 calls, 9.7M weighted -> +2 points (4.86M per point)
91% -> 93%
This morning's window spans every session and every project directory, sub-agent work included:
269 calls in one project, 5 and 3 in two others, 127 of the 277 delegated. It is the whole
machine, not one conversation.
Same account, same time of day, consecutive mornings, both following an overnight gap with the
machine powered off. Three times the work this morning cost thirteen times fewer quota points,
a factor of 41. Whatever is happening is not a function of the hour of the day, of peak load, or
of the size of the first message after a break: both windows share all three conditions.
THE SAME 100% THRESHOLD, TWO WEEKS RUNNING
Week Jul 27 17:00 -> Aug 3 17:00: I hit the weekly limit on Aug 2 at 13:22, after 8,165 calls
and 335.0M weighted tokens. The API error is in my transcripts four times, at Aug 2 13:22 and
14:17 and Aug 3 00:22 and 01:10: "You've hit your weekly limit · resets Aug 3, 5pm
(Pacific/Honolulu)". Work resumes Aug 3 at 17:08, eight minutes after the reset.
Week Aug 3 17:00 -> Aug 5 23:57: 2,413 calls and 113.8M weighted, counter reading 91%.
Extrapolated to the same hard stop, 100% arrives around 125M.
Reaching the identical limit took 335.0M one week and roughly 125M the next: 2.7x less work for
the same exhausted quota, same account, consecutive weeks.
One caveat I want to state myself rather than have it pointed out. The first window is 168 hours
and the second is 55 hours, so the first is not "heavier per hour"; it is three times longer and
did three times the total work. Per hour the two are nearly identical (1.99M/h against 2.07M/h).
The comparison that matters is not intensity but the total needed to exhaust one weekly
allowance, and that total fell by a factor of 2.7 from one week to the next.
What Should Happen?
A given amount of measured work should consume a predictable share of the weekly quota.
Two windows of the same day, on the same plan, build and account, should charge amounts per
token within the same order of magnitude. A window sending ten times more tokens should not cost
fewer quota points than the one before it.
And the total work needed to exhaust one weekly allowance should not fall by a factor of 2.7
from one week to the next with no change of plan.
Error Messages/Logs
The metering itself is silent, so this is the script output alongside the counter readings,
plus the one real error: the weekly limit hit on the previous week.
$ node usage-report.js "2026-08-05 07:59" "2026-08-05 09:00"
601 transcript files under C:\Users\<user>\.claude\projects
29560 usage lines reduce to 11476 API calls (factor 2.58)
window 2026-08-05 07:59 -> 2026-08-05 09:00 versions: 2.1.221
count deleg. write M read M out M weighted
CALLS (use this) 58 0 0.9 13.9 0.1 3.0
lines (inflated) 131 0 3.3 28.1 0.3 8.4
-> usage panel over this window: weekly 33% -> 59% (+26 points)
$ node usage-report.js "2026-08-05 09:00" "2026-08-05 17:00"
count deleg. write M read M out M weighted
CALLS (use this) 583 189 8.3 150.0 0.8 29.5
lines (inflated) 1366 546 23.2 314.3 1.7 69.0
-> usage panel over this window: weekly 61% -> 76% (+15 points)
The previous week's limit, from the transcripts (four occurrences, Aug 2 13:22 to Aug 3 01:10):
You've hit your weekly limit · resets Aug 3, 5pm (Pacific/Honolulu)
Steps to Reproduce
This is a metering defect rather than a crash, so these are steps to MEASURE it. They work on
any account.
- Save the script from Additional Information as usage-report.js.
- Open the usage panel and write down the weekly percentage with the exact clock time. Do this
BEFORE opening a session: opening one already consumes.
- Work normally for a bounded window, then read the counter again and note the time.
- Run:
node usage-report.js "YYYY-MM-DD HH:MM" "YYYY-MM-DD HH:MM"
with the two timestamps from steps 2 and 3.
- Divide the CALLS row's weighted figure by the number of quota points the counter moved.
That is your cost per quota point for that window.
- Repeat for a second window of the same day, ideally much larger than the first.
On my account those two figures came out 17x apart on Aug 5: 0.12M per point at 07:59-09:00
against 1.97M per point at 09:00-17:00.
WHAT WOULD SETTLE THIS FROM YOUR SIDE, AND I CANNOT DO IT FROM MINE:
For my account, list every quota-affecting event between Aug 3 17:00 and Aug 5 23:57 (UTC-10)
with its timestamp, the originating request, and the quota points it consumed.
The specific thing to look at: the +26 points that landed between 07:59 and 09:00 on Aug 5. On
my side, everything sent that morning before the counter moved amounts to 1.0M weighted tokens,
and the session it landed in totals 0.7M over its entire life. If the ledger attributes
materially more than that to those minutes, the difference is the bug, and its timestamp will
say which earlier work it was carried over from.
Claude Model
Not sure / Multiple models
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.219
Claude Code Version
2.1.221 (2.1.223 also present on this machine)
Platform
Anthropic API
Operating System
Windows
Terminal/Shell
Other
Additional Information
Environment
- Plan: Claude Max 20x. Overage purchase unavailable on the account (org_level_disabled), so the
weekly limit is a hard stop.
- Auth: OAuth subscription. No ANTHROPIC_API_KEY in the environment.
- Weekly reset: Monday 17:00, UTC-10, confirmed by the rate-limit message quoted above
("resets Aug 3, 5pm (Pacific/Honolulu)"). All times local.
- The usage panel shows three counters: 5-hour, weekly all-models, weekly Fable. Both weekly
counters show the behaviour.
Method, and one trap worth knowing about
Figures come from ~/.claude/projects/**/*.jsonl, all sessions and all projects.
A single API response is written as SEVERAL lines in the transcript, one per content block
(thinking, text, tool_use), and every one of those lines carries a copy of the SAME call's usage
object. Summing lines therefore bills one call several times. On this machine 29,560 usage lines
reduce to 11,476 actual API calls, a factor of 2.58, and the factor varies by day, so line counts
are not comparable across periods.
Verified before relying on it: across the 9,170 requestIds appearing more than once, the cache
numbers are byte-identical in 100% of cases and output_tokens never decreases, which is what you
would expect from repeated snapshots of one call. Only 7.6% of duplicated requestIds appear in
more than one file, so session forking is not the main cause.
Every figure in this report is a CALL count. The script prints both so you can see the gap on
your own data.
The single "weighted" column is cache_creation1.25 + cache_read0.1 + output*5. The weights
barely matter: every underlying column moves the same way.
Ruled out on this account, each with a measurement
- Prompt caching is working. Daily cache-write to cache-read ratio stayed between 2.8% and 6.6%
every day from Jul 27 to Aug 5, so the cache is being reused rather than rebuilt.
- Not sub-agents or workflows. Zero delegated calls in the 40 hours covering both jumps
(Aug 3 17:00 to Aug 5 09:00). The first delegated call of that week ran Aug 5 at 12:31, hours
after both. The previous week ran 4,736 of them and still needed 335.0M to reach the limit that
this week reached on roughly 125M.
- Not model mix. The +3-point window (Tue 17:00-23:00) ran 155 Opus calls; the +26-point window
(Wed 07:59-09:00) ran 44.
- Not context size. My two largest-context days were Jul 27 and Jul 28, averaging 376k and 512k
tokens of context per call, and the counter behaved normally then. Aug 4 and Aug 5 averaged
283k and 244k.
- Not elapsed time or an idle drain, tested twice. Two hours idle with the app open and the
machine on moved none of the three counters. A full night with the app closed and the machine
powered off (Aug 5 23:57 to Aug 6 07:03, zero calls in the transcripts) left weekly at 91% and
Fable at 85%.
- Not an API key misroute. No ANTHROPIC_API_KEY set; OAuth confirmed.
- Not provably the version change. The client went from 2.1.219 to 2.1.221 somewhere between
Aug 4 00:09 and 17:28, an interval containing no calls at all, and my first abnormal reading
falls inside it. Worth noting: the two weekly windows above do NOT share a build. Week 1 is
100% 2.1.219; week 2 is 2,306 calls on 2.1.221 against 107 on 2.1.219. The same-day comparison
is entirely on 2.1.221 and is unaffected.
The separate Fable counter shows the same shape
My plan meters a weekly Fable allowance separately, and it moved the same way on the morning of
Aug 5.
Fable work that morning, 07:59 to 09:00: 14 calls, 1.00M weighted, all in one session. The first
of them wrote 332,911 cache-creation tokens, which is the cache being rebuilt after the overnight
gap; the other thirteen are small, 0.04M each.
Same day, 09:00 to 17:00: 360 Fable calls, 17.60M weighted. Seventeen times the Fable work of
the morning.
I did not photograph the Fable counter between those two windows, so what follows is recollection
and not measurement: it read roughly 50% after that morning, from far lower before. If that is
right, half a weekly Fable allowance went to 1.00M of Fable work while the afternoon's 17.60M
cost visibly less. I raise it because the shape matches the all-models counter exactly, and
because #83579 reports both counters moving together.
The overnight Fable figure is firm: it stayed at 85% from Aug 5 23:57 to Aug 6 07:03 with nothing
sent.
A third episode, and this one matches the idle-drain reports
On Aug 2 I worked until 01:22 and stopped. From memory the weekly counter read about 90% when I
closed the app. Twelve hours later, having sent nothing in between, my first message returned the
weekly-limit error.
The 100% at 13:22 is documented by the API error in my transcripts. The 90% at 01:22 is my
recollection with no trace behind it, so I cannot rule out that I was already at 100% and did not
look. That makes this episode compatible with #83579, unlike the two idle periods I measured
deliberately afterwards, which were flat.
A hypothesis that would reconcile the two
#83579 and #82506 report the counter rising during idle periods. Mine does not, in the two
periods I measured. If the charge is computed with a delay and applied at the next request rather
than continuously, both observations are the same server-side behaviour seen at different moments:
someone reading the meter after opening the app sees "it rose while I was away", while I, reading
my counters before opening anything, see "it rose the instant I typed, by an amount unrelated to
my message".
That would also fit the week-boundary shape: 4,736 delegated calls ran Jul 27 to Aug 3, and the
following week filled up without matching local activity. If delegated usage is metered with a
delay that can cross a weekly reset, it would land on the wrong week.
I cannot test any of this from here.
One secondary finding, possibly its own bug
After a gap of more than one hour, the cache is rebuilt over three turns rather than one. Median
cache-write across 29 resumes: 412k on the resume, 412k on the next turn, 212k on the one after,
then ~9k. A resume costs roughly 1M tokens rather than 412k. Real, but it accounts for maybe
10-15% of a week, so it does not explain the above.
Session ids, all UTC-10
- 878f0610-6ec0-4678-9331-2079ae88dd2b — Aug 5, 08:05 to 08:29. The session the +26 landed in.
25 calls, 0.7M weighted (64 transcript lines).
- 7a7e38c0-d1d4-439d-9e81-34a319324890 — Aug 4 19:15 to Aug 6 08:04. The working session where I
first noticed the problem and ran most of the analysis. 298 calls, 19.2M weighted.
- 4d6f1bc9-00b9-4f33-88b2-b7995231c203 — Aug 5, 10:03 to 17:32. 285 calls, 17.3M weighted.
Support conversation 215475383633531, handled by the AI support agent only, which stated it
cannot inspect metering or restore quota. /feedback sent Aug 6 with these session ids.
Related
#83579 reports idle-period depletion on Max 20x affecting both weekly counters, and links #82506
and #54750. My measurements agree with theirs on one episode and disagree on two others, so I am
filing separately rather than adding a data point to that thread.
GitHub also flags #40881 as a possible duplicate. That one was closed as invalid and stale, and
I think the reason is instructive: it reported disproportionate consumption without a way for
anyone to reproduce the measurement. Every figure here comes out of the script above, which runs
on any account in a few seconds and prints the same columns.
The script
No dependencies, no network, nothing leaves your machine. It reads ~/.claude/projects/**/*.jsonl
and reports what was actually sent in a window, so it can be compared against what the counter
charged over the same window.
node usage-report.js # per local day
node usage-report.js "2026-08-05 07:59" "2026-08-05 09:00" # one window
<details>
<summary>usage-report.js</summary>
// usage-report.js — aggregate your own Claude Code usage from local transcripts.
// No dependencies, no network, nothing leaves your machine. Node 18+.
//
// node usage-report.js per local day
// node usage-report.js "2026-08-05 07:59" "2026-08-05 09:00" one window
//
// WHY THIS IS NOT A SIMPLE LINE COUNT.
// A single API response is written to the .jsonl as SEVERAL lines, one per content
// block (thinking, text, tool_use). Every one of those lines carries a copy of the
// SAME call's usage object, so summing lines bills one call two to ten times over.
// Verified on this machine: across 9,170 groups sharing a requestId, the cache
// numbers are byte-identical in 100% of them, and output_tokens never decreases.
// So we reduce by requestId and keep the largest snapshot, which is the completed
// call. 29,552 lines reduce to 11,472 actual API calls here, a factor of 2.6.
//
// Session forking also copies turns between files, but it is marginal: only 7.6%
// of duplicated requestIds appear in more than one file. The multi-line write is
// the real cause.
//
// The script prints both figures so you can see the gap on your own data. Use
// CALLS for anything you intend to quote. LINES is shown only to expose the trap.
const fs = require('fs'), path = require('path'), os = require('os');
const root = path.join(os.homedir(), '.claude', 'projects');
const files = [];
(function walk(dir) {
let entries; try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
for (const e of entries) {
const p = path.join(dir, e.name);
if (e.isDirectory()) walk(p);
else if (e.name.endsWith('.jsonl')) files.push(p);
}
})(root);
files.sort();
if (!files.length) {
console.error(`No transcripts found under ${root}`);
process.exit(1);
}
const pad = n => String(n).padStart(2, '0');
const stamp = t => `${t.getFullYear()}-${pad(t.getMonth() + 1)}-${pad(t.getDate())} ${pad(t.getHours())}:${pad(t.getMinutes())}`;
const lines = [];
for (const f of files) {
let txt; try { txt = fs.readFileSync(f, 'utf8'); } catch { continue; }
for (const line of txt.split('\n')) {
if (!line.startsWith('{')) continue;
let o; try { o = JSON.parse(line); } catch { continue; }
const u = o.message && o.message.usage;
if (!u || !o.timestamp) continue;
const t = new Date(o.timestamp);
if (isNaN(t)) continue;
lines.push({
t, id: o.requestId || null,
delegated: o.isSidechain === true || /[\\/](wf_|subagents[\\/])/.test(f),
version: o.version || '',
w: u.cache_creation_input_tokens || 0,
r: u.cache_read_input_tokens || 0,
o: u.output_tokens || 0,
});
}
}
// Reduce to one record per API call: largest snapshot wins.
const best = new Map(); const noId = [];
for (const x of lines) {
if (!x.id) { noId.push(x); continue; }
const cur = best.get(x.id);
if (!cur || x.o > cur.o) best.set(x.id, x);
}
const calls = [...best.values(), ...noId].sort((a, b) => a.t - b.t);
lines.sort((a, b) => a.t - b.t);
const weigh = a => a.w * 1.25 + a.r * 0.1 + a.o * 5;
const M = n => (n / 1e6).toFixed(1).padStart(9);
function tally(set, from, to) {
const a = { n: 0, d: 0, w: 0, r: 0, o: 0, versions: new Set() };
for (const x of set) {
const s = stamp(x.t);
if (from && s < from) continue;
if (to && s >= to) continue;
a.n++; a.w += x.w; a.r += x.r; a.o += x.o;
if (x.delegated) a.d++;
if (x.version) a.versions.add(x.version);
}
return a;
}
const row = (label, a) =>
`${label.padEnd(22)}${String(a.n).padStart(9)}${String(a.d).padStart(9)}${M(a.w)}${M(a.r)}${M(a.o)}${M(weigh(a))}`;
const header = `${''.padEnd(22)}${'count'.padStart(9)}${'deleg.'.padStart(9)}${'write M'.padStart(9)}${'read M'.padStart(9)}${'out M'.padStart(9)}${'weighted'.padStart(9)}`;
console.log(`${files.length} transcript files under ${root}`);
console.log(`${lines.length} usage lines reduce to ${calls.length} API calls (factor ${(lines.length / calls.length).toFixed(2)})\n`);
if (process.argv[2]) {
const [from, to] = [process.argv[2], process.argv[3]];
const c = tally(calls, from, to), l = tally(lines, from, to);
console.log(`window ${from} -> ${to || 'now'} versions: ${[...c.versions].join(', ') || 'n/a'}\n`);
console.log(header);
console.log(row('CALLS (use this)', c));
console.log(row('lines (inflated)', l));
console.log(`\nQuote the CALLS row. Divide its weighted figure by the quota points your`);
console.log(`counter moved over the same window to get your cost per quota point.`);
} else {
const days = [...new Set(calls.map(x => stamp(x.t).slice(0, 10)))].sort();
console.log(header);
for (const d of days) console.log(row(d, tally(calls, `${d} 00:00`, `${d} 23:59`)));
console.log(`\n${row('TOTAL (calls)', tally(calls))}`);
console.log(`\nPass two timestamps to measure one window, e.g.`);
console.log(` node usage-report.js "${days[days.length - 1]} 07:00" "${days[days.length - 1]} 09:00"`);
}
</details>
<img width="445" height="211" alt="Image" src="https://github.com/user-attachments/assets/f1c51be1-162d-4c8d-898b-42c9fde1e428" />
Showing cached comments. Read the full discussion on GitHub ↗
3 Comments
Same issue for me too, the max plan gets exhausted in 2 days, though the usage was similar to earlier
Ran this against an independent dataset to see whether the variance reproduces: 28 days, 32.7B tokens, six Max accounts, measured from transcripts with
ccusagerather than from the quota counter. Different setup, different timezone, no overlap with yours.The headline number matches yours almost exactly. Across all 28 days, cost per million tokens ranges 0.27 to 5.48, a 20x spread.
It collapses under a sample-size filter, though. Dropping days under 10M tokens leaves 24 days and the spread falls to 2.0x (0.75 to 1.53 per million). Every extreme came from a low-volume day, where a handful of calls sets the whole ratio.
What survives correlates cleanly with token composition rather than with time of day:
| | correlation with cost per Mtok |
|---|---|
| cache-read share | -0.87 |
| output share | +0.71 |
| cache-write share | +0.50 |
The two ends of the range, same account, same week:
Cache-read share moves 93% to 97.9% and output share doubles, and that alone accounts for the 2x. Composition is not stable across a working day: cache-read share in my data ranges 47.6% to 97.9%, and output share spans a factor of 700 between the quietest and busiest hours. A window with more short exchanges is structurally more expensive per token than a window that reads a large context repeatedly, even at identical model mix.
That is the part I would isolate before reading the counter as inconsistent. Your 07:59-09:00 window is 58 calls against 583 in the next one, so it sits exactly where my low-volume days produced spurious extremes. Two checks that would separate a real quota bug from composition drift:
Not disputing the report. Composition explains a 2x in my data and yours is an order of magnitude wider, so something may well remain after this is factored out. But the low-volume windows need to come out first, otherwise the effect size is not measurable.
Method:
ccusage daily --jsonacross sixCLAUDE_CONFIG_DIRroots, deduplicated by message id, days under 10M tokens excluded, cost from published per-bucket rates.Adding a Max 20x datapoint measured with the same transcript-dedup approach, from a fully-covered weekly window. It lands between your two weekly readings and supports the direction of the report.
Environment
organizationRateLimitTier: default_claude_max_20x),hasExtraUsageEnabled: false, so the weekly limit is a hard stop~/.claude/projects/**/*.jsonl, deduplicated bymessage.id(same trap you describe — the usage object repeats once per content block), Fable-model calls onlyWindow Aug 6 08:00 → Aug 13 08:00 (fully covered by transcripts), Fable only, 2,465 calls
| bucket | tokens |
|---|---|
| fresh input | 4,683 |
| cache_write | 19,624,113 (12.26M @1h TTL, 7.37M @5m) |
| cache_read | 679,163,543 |
| output | 2,285,312 |
Cache-read share of input-side tokens: 97.2%.
Your weighted formula (
write×1.25 + read×0.1 + out×5) gives 103.9M weighted, at which point Fable calls cease entirely: the last Fable call in the window is at hour 47 (Aug 8 07:09), and there is not a single Fable call for the remaining 121 hours — the rest of the week ran on other models. Caveat: this is cessation of calls in the transcripts, not a counter screenshot.Since that 103.9M exhausted the Fable sub-allowance (the 50% cap), not the all-models pool, it implies an all-models weekly pool of roughly 208M weighted on this account that week — between your 335M (week of Jul 27) and ~125M (week of Aug 3).
For contrast, the previous window (Jul 30 → Aug 6) still had Fable serving at window-hour 163, i.e. nearly the full week. Its measured total is only a lower bound (83.5M weighted — transcripts before Aug 2 were already cleaned up), so I can't state a week-over-week ratio from my data; the fall from "Fable lasts ~7 days" to "Fable lasts 47 hours" between consecutive windows is what I can state.
Per-day weighted inside the exhausting window: Aug 6: 47.0M, Aug 7: 42.6M, Aug 8 (partial, until cutoff): 14.3M.