# Cache disclosure surfaces

Every response header a hit sets, every client control a caller can send, and every field the decision ledger records about the cache.

Source: https://tokentriage.com/docs/build/caching-disclosure

---
import Callout from '../../../components/Callout.astro';
import Details from '../../../components/Details.astro';
import NextSteps from '../../../components/NextSteps.astro';

Three surfaces carry what the cache did: the response headers a client sees, the control headers a
client may send, and the `decision.response_cache` block on the ledger row. This page is the full
list of each.

Headers are covered generally in [response headers](/docs/reference/headers) and ledger fields in
[decision ledger fields](/docs/reference/ledger-fields); this page is the cache-specific detail.

## Response headers

`x-tt-cache` is set on every cache-eligible request. The rest appear on a served hit.

| Header | On | Values |
|---|---|---|
| `x-tt-cache` | every eligible request | `hit` · `miss` · `bypass` · `error` |
| `x-tt-cache-kind` | hit | `exact` · `normalized` · `semantic` |
| `x-tt-cache-age` | hit | age of the stored entry, in seconds |
| `x-tt-cache-source` | hit | the request id whose response this is |
| `x-tt-cache-entry` | hit | the entry id, and the handle a per-entry purge takes |
| `x-tt-cache-similarity` | semantic hit | the match score |
| `x-tt-cache-threshold` | semantic hit | the bar that score cleared |
| `x-tt-cache-verified` | semantic hit | `true` · `false` · `none` when no verifier ran |

There is no setting that suppresses them. They are written from the same value the ledger block is
written from, before any response byte goes out.

```console
HTTP/1.1 200 OK
Content-Type: application/json
X-Tt-Cache: hit
X-Tt-Cache-Age: 102
X-Tt-Cache-Entry: 3a2e3889d0a87a520dce119a216f57b64631b2514a094c71d65f078a67452f23
X-Tt-Cache-Kind: exact
X-Tt-Cache-Source: tt_3fg5aqubovpekklypzj5fxq3de
X-Tt-Mode: route
X-Tt-Request-Id: tt_n67vihukwdsdhxma57ugkvzm74
```

<Callout type="warning">
`x-tt-cache` describes what the client received, which is not the same vocabulary as the row's
lookup outcome. A shadow `would_hit` row pairs with an `x-tt-cache: miss` header, because the
response was a live passthrough. Report hit rate from rows.
</Callout>

`x-tt-decision` is absent on a hit: no upstream was contacted for that response.

### What is replayed alongside them

The stored entry carries the response headers the client saw, minus the ones that must be
recomputed or must not be reused: framing and length, content encoding, the authentication headers,
and the whole `x-tt-*` namespace. Those are stamped fresh at replay, so a replayed response carries
its own request id rather than the original's.

## Client control headers

A caller sends `x-tt-cache` on the request to change what the cache does for it. Each control's
true effect is recorded on the row.

| Sent | Effect | Row records |
|---|---|---|
| `off` | no read, no write | `outcome: bypass`, `bypass_reason: client_off` |
| `refresh` | no read, write allowed | `outcome: miss`, `stored: true` |
| `no-store` | read allowed, no write | a normal hit or miss, `stored: false` |
| anything else | treated as no control | as though the header were absent |

These are TokenTriage control headers and are stripped before the request is forwarded; the provider
never sees them.

Two behaviours are worth knowing:

**`refresh` replaces the entry in place.** The entry id is derived from the scope and fingerprint, so
it does not change. What changes is the stored payload and its provenance — after a refresh,
`x-tt-cache-source` on later hits names the refreshing request.

**A `no-store` hit is still a hit.** The control skips the write, not the disclosure. The response
carries the full hit header set.

An unrecognised value fails open to no control, so a client typo cannot silently disable caching for
a caller.

## The ledger block

`decision.response_cache` is present on every row where the cache engaged: a lookup ran, a store
happened, or a bypass was decided. Its absence means the cache did not touch the request.

```json
"response_cache": {
  "outcome": "hit",
  "kind": "exact",
  "rule": "chat-cache",
  "strategy": "exact",
  "entry_sha7": "3a2e388",
  "source_req_id": "tt_3fg5aqubovpekklypzj5fxq3de",
  "age_ms": 102885,
  "scope_sha7": "799cda7",
  "stored_provider": "openai",
  "stored_model": "gpt-4o-mini",
  "source_usage": { "input_tokens": 1200, "output_tokens": 350, "total_tokens": 1550 },
  "saved": { "usd": 0.00039, "estimated": true, "unpriced": false },
  "saved_basis": "source_usage repriced @ litellm@2026-07-14#179affb, fixed usage",
  "stored": false,
  "lookup_us": 518,
  "serving_tier": "l2"
}
```

| Field | On | Meaning |
|---|---|---|
| `outcome` | always | `hit` · `would_hit` · `miss` · `bypass` · `error` |
| `rule` | when a rule was selected | the rule id that governed |
| `strategy` | hit, would_hit | the matcher that matched |
| `kind` | hit, would_hit | the disclosure kind of the match |
| `entry_sha7` | hit, would_hit | first seven characters of the entry id |
| `scope_sha7` | hit, would_hit | first seven characters of the scope key |
| `source_req_id` | hit, would_hit | the request whose response is stored |
| `age_ms` | hit, would_hit | age of the entry at match time |
| `stored_provider`, `stored_model` | hit, would_hit | the identity that generated the payload |
| `source_usage` | hit, would_hit | the source row's usage, the savings input |
| `saved`, `saved_basis` | hit, would_hit | the repriced counterfactual and how it was computed |
| `serving_tier` | hit, would_hit | `l1` · `l2` · `l3` |
| `similarity`, `threshold` | semantic | the score and the effective bar |
| `verified` | semantic, when a verifier ran | the gray-band verdict |
| `recall_exact`, `recall_config` | semantic | false plus a config string when an approximate index recalled |
| `verified_active`, `verified_calibrated`, `verified_samples`, `verified_bound` | semantic under a calibration policy | the calibration state behind the effective threshold |
| `embed_cost` | metered embedder | the cache's own priced spend for this lookup |
| `stored` | always | whether this row's response was written to the store |
| `bypass_reason` | bypass, and an over-budget miss | the closed reason vocabulary |
| `lookup_us`, `embed_us` | when measured | lookup and embedding time |

<Details summary="Fields whose absence is meaningful">

`verified` is absent when no verifier ran, which is the normal case for a confident-zone semantic
hit and for every exact or normalized hit. It is not the same as `verified: false`, which means a
verifier ran and did not pass.

`embed_cost` is absent for any lookup that did not embed and for any in-process embedder, which
records no spend rather than a zero.

`source_usage` is absent when the stored entry carried no usage.

`recall_exact` is present only when a similarity decision was made.

</Details>

## The top-level fields a hit row changes

Two, and both matter for anything that aggregates rows.

```json
"usage": { "input_tokens": 0, "output_tokens": 0, "total_tokens": 0, "source": "replay" },
"cost":  { "usd": 0, "pricing_snapshot": "litellm@2026-07-14#179affb", "estimated": false }
```

`usage.source: "replay"` with zero buckets is what keeps replayed tokens from being counted as fresh
spend. The source row's tokens are disclosed inside the cache block instead, where a token
aggregation will not pick them up.

`cost.usd: 0` on a hit row is a confident zero, which is correct here: no upstream call was made, so
no money was spent. It is not the [UNPRICED](/docs/concepts/honesty-model) case, where a price could
not be resolved.

The row still carries the routing decision (`decision.tier`, `decision.upstream`) because routing
ran before the cache lookup. On a hit those name the route the request would have taken.

<NextSteps items={[
  {title: "Decision ledger fields", href: "/docs/reference/ledger-fields", description: "Every other field on the row this block sits inside."},
  {title: "Response headers", href: "/docs/reference/headers", description: "The full x-tt-* header contract, including the identity and trace headers."},
  {title: "Why didn't this request hit?", href: "/docs/build/caching-misses", description: "Using these fields to work back from an unexpected miss."}
]} />
