Claude Code Status Line: Prompt Cache Fields
Claude Code v2.1.251 added a prompt_cache object to the JSON your status line script receives, plus a rate_limits.spend_limit window. Together they let a one-line status bar answer the question that actually costs money: is the cache warm right now, and did something just invalidate it.
This is the field reference plus two working scripts. Every field name, type, and null condition below is read from the Claude Code status line and cost docs on August 31, 2026, and both scripts were run against the docs' own example payload and five edge-case variants before publishing.
The short answer
prompt_cacheis a top-level field in the status line JSON, a sibling ofcostandcontext_window, not nested insidecost.- It appears only after the main conversation's first API response. Before that it is absent, so read it defensively.
warmandcaching_observedare two different flags.caching_observedfalse means caching is off or your provider does not report it.caching_observedtrue withwarmfalse means the cache existed and went cold.missesandexpected_rebuildscount the same kind of event with different causes. Rebuilds after/compactland inexpected_rebuilds, so do not alarm on them.hit_ratioruns 0 to 1 and its denominator includes cache writes, so a healthy long session sits well below 1.0.
Where prompt_cache sits in the payload
Claude Code pipes a JSON object to your status line command on stdin. The cache-related parts of it look like this:
{
"model": { "id": "claude-opus-5", "display_name": "Opus" },
"context_window": {
"context_window_size": 200000,
"used_percentage": 8,
"current_usage": {
"cache_creation_input_tokens": 5000,
"cache_read_input_tokens": 2000
}
},
"prompt_cache": {
"warm": true,
"caching_observed": true,
"ttl": "1h",
"expires_at": 1738429200,
"requests": 14,
"misses": 2,
"expected_rebuilds": 1,
"hit_ratio": 0.91,
"cache_write_tokens": 352000,
"miss_recache_tokens": 310200,
"last_miss_at": 1738425230,
"recache_tokens_if_cold": 45000
},
"rate_limits": {
"spend_limit": { "used_percentage": 62.8, "resets_at": 1740787200 }
}
}
The nesting is worth stating plainly because it is easy to guess wrong: cache statistics are not under cost. The cost object holds only total_cost_usd, total_duration_ms, total_api_duration_ms, total_lines_added, and total_lines_removed. Read .prompt_cache.hit_ratio, never .cost.prompt_cache.hit_ratio.
All twelve prompt_cache fields
| Field | Meaning |
|---|---|
warm | Whether the cached prefix is still inside its TTL. False when the last response reported no cache tokens, even while caching_observed is true |
caching_observed | Whether any response this session reported cache tokens. False means caching is off, or your provider or gateway does not report it |
ttl | Cache lifetime of the current prefix: "5m" or "1h" |
expires_at | Epoch seconds when the prefix goes cold. Null when the last response reported no cache tokens |
requests | API requests recorded for the main conversation this session |
misses | Requests that re-processed content the cache already held (see the threshold below) |
expected_rebuilds | Rebuilds that followed a compaction or a clearing of old tool results |
hit_ratio | Cache read tokens as a fraction of all input tokens, 0 to 1. Null while those counts are all zero |
cache_write_tokens | All tokens written to the cache this session, including the first request's initial write |
miss_recache_tokens | Tokens written to the cache by the requests counted as misses |
last_miss_at | Epoch seconds of the last miss. Null while the session has no misses |
recache_tokens_if_cold | Tokens the next request re-caches if the cache has gone cold by then. Null right after a compaction until the next request records the rewritten size |
All timestamps are Unix epoch seconds, the same unit as rate_limits.*.resets_at.
Two scope limits matter when you interpret these numbers. Claude Code computes them from the cache token counts in the API's responses, so they work on every provider and gateway rather than only on first-party API keys. And they cover the main conversation only: subagent requests are not counted, so a session running heavy subagents shows cache statistics for the parent thread alone.
What counts as a miss
This is the field most likely to be misread, because "2 misses" looks like a failure and often is not.
Claude Code counts a request as a miss when it re-processed more than 5% and at least 2,000 tokens of what it could have read from cache, with no compaction or tool-result clearing to explain the shortfall in cache reads. Both parts of that threshold matter: small prefix churn does not register, and a genuine invalidation on a large conversation does.
When Claude Code has itself just rewritten the conversation, by compaction or by clearing old tool results from context, the same event is counted as an expected_rebuild instead. That separation is the useful part of the design. A rising misses count means something in your prefix is moving (a model switch, an effort change, an MCP server connecting, a plugin toggle). A rising expected_rebuilds count means Claude Code compacted, which was going to cost a rebuild no matter what.
So a status line should surface misses and stay quiet about expected_rebuilds. Both scripts below do exactly that.
Why hit_ratio is not 1.0 on a healthy session
hit_ratio is cache read tokens over all input tokens this session, and the denominator counts cache reads, cache writes, and uncached input. Every write you pay for to establish the cache is in the bottom of that fraction.
Which means the number starts low and climbs. A session on its first request has written the cache and read nothing from it, so the ratio is near zero while nothing is wrong. A long session that keeps its prefix stable trends up toward the high 0.8s and 0.9s. Treat a sustained drop as the signal, not an absolute threshold, and read it next to misses rather than alone.
The field is null while cache reads, writes, and uncached input are all zero, so hit_ratio == null is a real state your script has to render.
A tested bash status line
Save as ~/.claude/statusline.sh, chmod +x it, then point statusLine.command at it in your settings.
#!/bin/bash
input=$(cat)
MODEL=$(echo "$input" | jq -r '.model.display_name')
CTX=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
# prompt_cache is absent until the main conversation's first API response.
CACHE=$(echo "$input" | jq -r '
if .prompt_cache == null then "cache -"
elif .prompt_cache.caching_observed == false then "cache n/a"
elif .prompt_cache.warm then
"cache "
+ (if .prompt_cache.hit_ratio == null then "?"
else ((.prompt_cache.hit_ratio * 100) | round | tostring) + "%" end)
+ " warm " + .prompt_cache.ttl
else
"cache cold"
+ (if .prompt_cache.recache_tokens_if_cold == null then ""
else " (+" + ((.prompt_cache.recache_tokens_if_cold / 1000) | floor | tostring) + "k next)" end)
end')
# Misses only. expected_rebuilds are compaction, not a problem to flag.
MISS=$(echo "$input" | jq -r '
if (.prompt_cache.misses // 0) > 0
then " · " + (.prompt_cache.misses | tostring) + " miss"
+ (if .prompt_cache.misses > 1 then "es" else "" end)
else "" end')
SPEND=$(echo "$input" | jq -r '
if .rate_limits.spend_limit == null then ""
else " · spend " + (.rate_limits.spend_limit.used_percentage | round | tostring) + "%"
end')
printf '%s' "$MODEL"
[ -n "$CTX" ] && printf ' · ctx %s%%' "$CTX"
printf ' · %s%s%s' "$CACHE" "$MISS" "$SPEND"
The same thing in Node
No jq dependency, and easier to extend.
#!/usr/bin/env node
let raw = '';
process.stdin.on('data', (d) => (raw += d));
process.stdin.on('end', () => {
const d = JSON.parse(raw);
const parts = [d.model.display_name];
const ctx = d.context_window?.used_percentage;
if (ctx != null) parts.push(`ctx ${ctx}%`);
const pc = d.prompt_cache;
if (!pc) {
parts.push('cache -');
} else if (!pc.caching_observed) {
parts.push('cache n/a');
} else if (pc.warm) {
const pct = pc.hit_ratio == null ? '?' : `${Math.round(pc.hit_ratio * 100)}%`;
parts.push(`cache ${pct} warm ${pc.ttl}`);
} else {
const next =
pc.recache_tokens_if_cold == null
? ''
: ` (+${Math.floor(pc.recache_tokens_if_cold / 1000)}k next)`;
parts.push(`cache cold${next}`);
}
// Misses only. expected_rebuilds are compaction, not a problem to flag.
if (pc?.misses > 0) parts.push(`${pc.misses} miss${pc.misses > 1 ? 'es' : ''}`);
const spend = d.rate_limits?.spend_limit?.used_percentage;
if (spend != null) parts.push(`spend ${Math.round(spend)}%`);
process.stdout.write(parts.join(' · '));
});
What the two scripts print
We ran both against the docs' example payload and five variants. The outputs are byte-identical between the bash and Node versions on every case:
| Session state | Status line output |
|---|---|
| Docs example (warm, 1h TTL, 2 misses, 1 expected rebuild) | Opus · ctx 8% · cache 91% warm 1h · 2 misses · spend 63% |
Before the first API response (prompt_cache absent) | Opus · ctx 8% · cache - |
Observed, then went cold (warm false, expires_at null) | Opus · ctx 8% · cache cold (+45k next) · 4 misses · spend 63% |
Provider reports no cache tokens (caching_observed false) | Opus · ctx 8% · cache n/a · spend 63% |
| Clean session, no misses, post-compaction nulls | Opus · ctx 8% · cache 88% warm 1h · spend 63% |
| Over the gateway spend limit | Opus · ctx 8% · cache 91% warm 1h · 2 misses · spend 103% |
The last row is not a rendering bug. spend_limit.used_percentage runs 0 to 100 and then above 100 once you exceed the limit, so do not clamp it.
The spend_limit window
rate_limits carries up to three windows: five_hour, seven_day, and, new in v2.1.251, spend_limit. Each has used_percentage and resets_at.
Three absence rules apply, and they bite scripts that assume the object exists:
rate_limitsappears only for Claude.ai Pro and Max subscribers, or behind a Claude apps gateway that sets a spend limit for you, and only after the first API response in the session.- Each window may be independently absent. Do not read
seven_daybecausefive_hourwas there. - Claude Code drops a window once its
resets_attime has passed.
The docs' own recommendation for bash is jq -r '.rate_limits.five_hour.used_percentage // empty', which yields an empty string rather than the text null.
Reading the same numbers in the terminal
You do not need a status line to see this. After the main conversation's first API response, Claude Code adds a Prompt cache (main) line to the Session block, which looks like this:
Prompt cache (main): 14 requests · 91% of input tokens from cache · 2 misses (last 6m 10s ago, 310.2k tokens re-cached) · 1 expected rebuild (compaction or tool-result clearing) · warm (1h TTL, last activity 40s ago)
The expected-rebuild segment appears only once at least one has happened. When the cache is cold the line reports how long the session has been idle, and when no response has reported cache tokens it ends with no prompt caching reported by the API instead. /clear resets it along with the rest of the Session block.
One naming wrinkle to save you a search: the v2.1.251 changelog describes this as "a per-session prompt-cache line to /cost", while the cost documentation puts the Session block under /usage and does not mention /cost anywhere. Treat /usage as the current name and the changelog wording as the older one for the same screen.
FAQ
Does this work on Bedrock, Vertex, or through a gateway? Yes. The statistics are computed from the cache token fields in the API's responses rather than from a first-party endpoint, so they work on every provider and gateway. If your gateway strips those fields you get caching_observed: false, which is the state the scripts above render as cache n/a.
Why is prompt_cache missing when my status line first renders? It appears only after the main conversation's first API response. Status lines render before that, so the field is genuinely absent and any script that dereferences it unguarded will throw on the first paint.
Do subagents count toward these numbers? No. The object covers the main conversation only. That is a deliberate scope choice, not a gap, and it means a subagent-heavy session can look cache-healthy at the parent level while spending elsewhere.
What minimum version do I need? Claude Code v2.1.251 or later for both prompt_cache and rate_limits.spend_limit. On earlier versions both are simply absent, which the scripts above handle as cache - and a missing spend segment.
How do I stop the misses in the first place? Misses come from prefix invalidation, and the causes are enumerable: switching model or effort, toggling fast mode, an MCP server connecting or disconnecting, enabling or disabling a plugin that ships one, denying a whole tool, and upgrading Claude Code. Our prompt caching guide covers each one and the TTL settings, and the model switch hooks article shows how to gate the most expensive of them before it happens.