I had August 31 circled in my notebook. That was the day Sonnet 5's introductory pricing was supposed to end.
I sat down to redo my September forecast, lined up recent usage totals, and swapped the rates for $3 / $15. At the scale an indie developer works at, a few tens of dollars a month is not noise. The number came out higher than I expected, so I went back to the primary source to double-check.
The official pricing page said something different. The $2 / $10 introductory rate is now the standard price. The increase scheduled for September 1 will not happen.
I published an article in July built entirely on that increase. The premise is gone. The table I'd just rebuilt was useless — but reading the price list line by line turned up a footnote I'd been skipping for months. That footnote is what this post is really about.
Check the current rates yourself
First, the facts. Here is the official price list as of August 23, 2026, in USD per million tokens.
| Model | Input | 5m cache write | Cache read | Output |
|---|---|---|---|---|
| Claude Fable 5 | $10 | $12.50 | $1 | $50 |
| Claude Opus 5 | $5 | $6.25 | $0.50 | $25 |
| Claude Sonnet 5 | $2 | $2.50 | $0.20 | $10 |
| Claude Sonnet 4.6 | $3 | $3.75 | $0.30 | $15 |
| Claude Haiku 4.5 | $1 | $1.25 | $0.10 | $5 |
Cache rates are derived mechanically from the base input rate: a 5-minute cache write costs 1.25x input, a 1-hour cache write costs 2x, and a cache read (hit) costs 0.1x. The Batch API halves input and output. Pinning inference_geo to "us" multiplies every category by 1.1.
These numbers move, so open the Claude Platform pricing page before you make a decision based on them. Not checking monthly is exactly how I ended up wrong in July.
Compute your effective cost from your own usage
Staring at a rate table is slower than plugging in your own numbers. Every API response carries a usage object with four counts:
input_tokens— input that did not come from cachecache_creation_input_tokens— input written to cachecache_read_input_tokens— input served from cacheoutput_tokens— tokens generated
Keep those four separate when you aggregate. It pays off in a moment. This script turns them into a cost breakdown.
M = 1_000_000
# tokenizer: "new" = Claude 4.7 and later, "old" = Claude Sonnet 4.6 and earlier
RATES = {
"sonnet-5": {"in": 2.0, "write5m": 2.50, "read": 0.20, "out": 10.0, "tokenizer": "new"},
"opus-5": {"in": 5.0, "write5m": 6.25, "read": 0.50, "out": 25.0, "tokenizer": "new"},
"sonnet-4-6": {"in": 3.0, "write5m": 3.75, "read": 0.30, "out": 15.0, "tokenizer": "old"},
"haiku-4-5": {"in": 1.0, "write5m": 1.25, "read": 0.10, "out": 5.0, "tokenizer": "old"},
}
def breakdown(model, usage, batch=False, us_only=False):
"""Return a cost breakdown. `usage` uses the same keys as the API response."""
r = RATES[model]
mult = (0.5 if batch else 1.0) * (1.1 if us_only else 1.0)
items = {
"uncached_input": usage.get("input_tokens", 0) * r["in"],
"cache_write": usage.get("cache_creation_input_tokens", 0) * r["write5m"],
"cache_read": usage.get("cache_read_input_tokens", 0) * r["read"],
"output": usage.get("output_tokens", 0) * r["out"],
}
items = {k: v * mult / M for k, v in items.items()}
items["total"] = sum(items.values())
return items
if __name__ == "__main__":
daily = { # replace with your own totals
"input_tokens": 120_000,
"cache_creation_input_tokens": 800_000,
"cache_read_input_tokens": 4_200_000,
"output_tokens": 260_000,
}
b = breakdown("sonnet-5", daily)
for k in ("uncached_input", "cache_write", "cache_read", "output", "total"):
print(f" {k:<15} ${b[k]:.4f} / day ${b[k] * 30:7.2f} / 30 days")Running it gives:
uncached_input $0.2400 / day $ 7.20 / 30 days
cache_write $2.0000 / day $ 60.00 / 30 days
cache_read $0.8400 / day $ 25.20 / 30 days
output $2.6000 / day $ 78.00 / 30 days
total $5.6800 / day $ 170.40 / 30 days
The breakdown matters more than the total, because each line points at a different fix.
If output dominates, look at max_tokens and your response format. If cache writes dominate, suspect your cache breakpoints, or gaps between requests that exceed the 5-minute TTL. If cache reads dominate, that is the healthy shape — the 0.1x multiplier is doing its job. A single total number hides all three.
For reference, the same usage at $3 / $15 comes to $255.60 over 30 days. The cancelled increase is worth $85.20 a month in this example.
Comparing models by rate alone will mislead you
What actually stopped me was a two-line footnote under the table.
Models from Claude 4.7 onward use a newer tokenizer that produces roughly 30% more tokens for the same text. Claude Sonnet 4.6 and earlier use the previous tokenizer.
So Sonnet 5 and Opus 5 count one way, while Sonnet 4.6 and Haiku 4.5 count another. Lining them up by dollars-per-million-tokens compares two different rulers.
To model a switch honestly, scale the token counts too:
def swap_model(src, dst, usage, tokenizer_ratio=1.30):
"""Cost of moving usage measured on `src` over to `dst`."""
a, b = RATES[src]["tokenizer"], RATES[dst]["tokenizer"]
scale = 1.0 if a == b else (1 / tokenizer_ratio if a == "new" else tokenizer_ratio)
scaled = {k: v * scale for k, v in usage.items()}
return breakdown(dst, scaled)["total"], scaleApplied to the same daily numbers:
haiku-4-5:
rate swap only $ 85.20 / 30 days (0.50x of Sonnet 5)
with 0.77 token scale $ 65.54 / 30 days (0.38x of Sonnet 5)
opus-5:
rate swap only $ 426.00 / 30 days (2.50x of Sonnet 5)
with 1.00 token scale $ 426.00 / 30 days (2.50x of Sonnet 5)
Haiku 4.5 is not "half the rate, therefore half the bill." In this example it lands at 0.38x. Opus 5 shares a tokenizer with Sonnet 5, so no scaling applies and it stays at 2.5x.
Cost is not the only thing affected. If the same text becomes 1.3x the tokens, the same context window holds proportionally less. If you run close to the window limit, you will feel that before you feel the billing difference.
Measure the 30% against your own text
Let me be straight about one thing: 1.30 is the figure the documentation gives as an approximation. I did not measure it. The real ratio depends on what you are sending — Japanese versus English, code versus prose.
Measuring your own input is more reliable, and the token counting endpoint is not billed. Count the same text on both models and take the ratio.
import anthropic
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from the environment
def count(model, text):
r = client.messages.count_tokens(
model=model,
messages=[{"role": "user", "content": text}],
)
return r.input_tokens
sample = open("prompts/typical_request.txt", encoding="utf-8").read()
new_tok = count("claude-sonnet-5", sample)
old_tok = count("claude-haiku-4-5", sample)
print(f"new={new_tok} old={old_tok} ratio={new_tok / old_tok:.3f}")Feed the result into tokenizer_ratio and the estimate becomes yours rather than a generic one. Three representative requests averaged together is enough to work with.
If you want to size a prompt before sending it, I wrote about that in The 200K-Token Cliff That Doubled My Nightly Bill.
One thing worth doing today
Sum the last seven days of usage and run it through the script above. You will see which of the four lines is largest.
That alone tells you where to work next. For me it was cache writes, which led me to a stretch of the day where request intervals were crossing the 5-minute cache TTL. Reading the rate table would never have surfaced that.
Once hand-editing constants every time pricing shifts gets tedious, the next step is attaching validity windows to the rates themselves. That is what my July piece, Effective-Dated Cost Forecasting Around the Sonnet 5 Introductory Price Expiry, sets out to build. There is some irony in that: the September 1 step it was designed around never arrived. The underlying conclusion — never store prices as static constants — held up better than my forecast did.
What I failed to account for was that a scheduled change can be withdrawn, not just applied. If this saves someone else the same oversight, that is enough.