# Read request coverage and freshness

Tell a request not yet indexed from a missing one, read how current an answer is, and retry or refresh without resending traffic.

Source: https://tokentriage.com/docs/operate/request-coverage

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

Your application got a `200` and an `x-tt-request-id` a few seconds ago, and the receipt
read says the service cannot find it. Or a dashboard's token total looks low for a busy
hour. Both are the same question: how far has the hosted index caught up with the traffic
your gateways served, and is this answer complete?

This page shows where each hosted answer states that, how to tell "not indexed yet" from
"not here", and which of retry, refresh or a different question fixes it. It uses the
`tokentriage cloud` CLI; the SDKs return the same documents.

Read [Trace one request from its ID](/docs/build/observability-request-id) first if you
have not read a receipt before.

## What you need

| You need | Because |
|---|---|
| your organization ID (`org_…`) and workspace ID (`t_…`) | every read here is scoped to one workspace |
| a management key in `TOKENTRIAGE_MANAGEMENT_KEY` with `metadata.read` | it gates every read on this page |
| `cost.read`, if you want money figures | without it, money cells read `unavailable (scope_not_authorized)` |

You do not need operator access to the analytics service, a log file, or a runtime key.
Nothing on this page sends a model request.

## Runtime success is not index success

A gateway answers your application first and records the request afterwards. The gateway
writes the request to its journal, the journal ships to the analytics service, and only then
can `customer.requests.get` or an insight query see it. A `200` with a request ID proves the
provider answered. It says nothing yet about whether the hosted index has the row.

So the first read of a fresh request can miss, and a window that ends "now" can undercount.
Neither means the request was lost. Resending the model call to "fix" a read creates a
second, paid request and leaves the first one exactly where it was.

<Callout type="warning">

Do not retry a paid model request because an analytics read came back incomplete. Retry the
read.

</Callout>

## 1. Read the read context

Every windowed answer (`insights overview`, `insights drilldown`) carries a `read_context`:
the conditions the answer was computed under. Ask with `--json` to see it:

```console
$ tokentriage cloud insights overview \
    --metric requests.v1 --metric provider_spend.v1 \
    --metric cost_per_1000_requests.v1 --metric tokens.v1 \
    --metric response_latency.v1 \
    --org org_K11111111111 --workspace t_K11111111111 \
    --from 2026-09-20T12:00:00Z --to 2026-09-21T12:00:00Z \
    --base-url http://127.0.0.1:48252 --json
```

```json
"read_context": {
  "from": "2026-09-20T12:00:00Z",
  "to": "2026-09-21T12:00:00Z",
  "arrival_fence": "918273",
  "watermarks": [
    {
      "producer_class": "gateway_journal",
      "produced_through": "918290",
      "indexed_through": "918273",
      "last_indexed_at": "2026-09-21T11:58:30Z"
    },
    {
      "producer_class": "control_plane_outbox",
      "produced_through": "41902",
      "indexed_through": "41902",
      "last_indexed_at": "2026-09-21T11:59:48Z"
    }
  ],
  "evaluated_at": "2026-09-21T12:00:00Z"
}
```

That is the `read_context` from the command's output, trimmed: the full block also carries
the account, workspace, scope digest, catalog and filter versions, purposes, and the
authority, projection, retention and exclusion generations. The command ran against a
loopback server replaying the recorded `overview_unpriced_spend` case in
`sdk/spec/insight-read-fixtures.json`, so the port and every value are that recording's.

Read it in this order:

- **`arrival_fence`** is the position in the ingest arrival counter this answer read up to.
  Every row that arrived after it is outside the answer, even if its request happened inside
  the window. It is a counter, not a time.
- **`watermarks`** gives one entry per producer class. `gateway_journal` is request traffic
  from your gateways; `control_plane_outbox` is control-plane events such as releases and app
  feedback. `produced_through` is how far the producer has sent, `indexed_through` how far the
  service has stored. Here the journal is 17 positions behind.
- **`last_indexed_at`** is when that class last indexed anything. A class with nothing
  indexed has unmeasured freshness, even if both counters read zero.
- **`evaluated_at`** is when the answer was computed.

A gap between `produced_through` and `indexed_through` is lag, not loss: those rows are on
their way. `produced_through` can itself be a floor, so a gap of one can mean "at least one
more".

Without `--json`, the CLI states the same facts on stderr so a pipeline keeps working. The
same replay without `--json`:

```console
application	requests.v1	1284 count
application	provider_spend.v1	unavailable (unpriced_population)
application	cost_per_1000_requests.v1	unavailable (unpriced_population)
application	tokens.v1	84210773 tokens (FLOOR: ingest_gap:gateway_journal)
application	response_latency.v1	412 milliseconds
```

```text
cloud: customer.insights.query POST http://127.0.0.1:48251/portal/api/v1/customer/v1/organizations/org_K11111111111/workspaces/t_K11111111111/insights/query
cloud: coverage is incomplete for this window (ingest_gap:gateway_journal); measured values use an incomplete population; counts/sums are lower bounds, ratios/percentiles may move either way
cloud: gateway_journal is behind (produced 918290, indexed 918273), so the most recent traffic may be missing
```

## 2. Read the coverage reasons

Coverage is stated in two places, with two small vocabularies.

**On a windowed answer**, `coverage` lists why the population is incomplete, such as
`ingest_gap:gateway_journal` above. A cell computed over that population comes back
`partial`, with the number and the same coverage list:

- counts and sums (`requests.v1`, `tokens.v1`, spend) are **lower bounds**: the true value is
  at least this;
- ratios and percentiles (cost per request, latency quantiles) are **not** bounds: they can
  move up or down as missing rows arrive.

**On a request receipt**, `coverage.unstated` names members the answer could not fill, each
with one of three reasons:

| reason | means |
|---|---|
| `not_projected` | the hosted store has no column for this member at all, for any request |
| `not_observed` | the column exists and holds nothing for this request |
| `not_authorized` | the member was withheld for want of a grant |

A receipt's `coverage` describes that one record. It does not say whether the index as a
whole is current; the read context on a windowed answer does.

## 3. Tell "not indexed yet" from "not found"

A by-ID read has no window of its own, so the hosted service searches the last 90 days of
indexed arrivals for it. When it finds nothing, it does not answer "no such request", because
an arrival counter cannot prove a request never happened. The answer is:

- **HTTP 503, `type=dependency_unavailable`**, with a `Retry-After` header (30 seconds on this
  build), and a detail that the request "has not been projected into the analytics store yet",
  naming the arrival position searched and the date range scanned.

`not_found` (HTTP 404) is reserved for two cases: the workspace in the path is not one your
key can see, or the record the index returned belongs to a different workspace than the path.

An ID from another workspace gets the same retryable 503 as an ID that is not indexed yet.
That is concealment: a different answer would confirm the ID exists somewhere. So use time
to tell them apart:

| Situation | Likely cause | What to do |
|---|---|---|
| a request from the last few minutes misses, then appears | index lag | retry after `Retry-After` |
| misses persist over several intervals while a current window's `gateway_journal` watermark shows no gap | wrong workspace, or a mistyped ID | check the org, workspace and ID against the ticket |
| the ticket is older than 90 days | outside the searched range, which the detail names | the by-ID read cannot reach it |

_Not yet executed against a deployment; output omitted._

```console
$ tokentriage cloud request get \
    --org $TT_ORG --workspace $TT_WORKSPACE --request $TT_REQUEST
```

<Details summary="Where the miss wording on this page comes from">

The status, `type`, 30-second interval, 90-day range and detail wording were read from
`internal/control/cmd/controlplaned/foundation_insights.go` and
`internal/central/analytics/chquery/request.go` at this page's date. No miss was captured for
this page, so the exact text your deployment prints may differ.

</Details>

## 4. Retry or refresh

Each refusal names its remedy, and they are not interchangeable.

| Answer | Remedy | What changes |
|---|---|---|
| HTTP 503 `dependency_unavailable` with `Retry-After` | wait the interval, then send the same read | nothing but time; do not loop faster than the header asks |
| HTTP 503 `coverage_unknown` | retry once the ingest frontier has moved | the service could not bound what is missing, so it gave no figure at all |
| HTTP 410 `read_context_expired` | drop the `read_context` and ask again (`refresh_view`) | a new snapshot: counts may differ from the page you were reading |
| HTTP 410 `snapshot_expired` | start a new query | the same, for a paged read |

`coverage_unknown` from the CLI, replaying the recorded `coverage_unknown_503` case:

```console
$ tokentriage cloud insights overview --metric requests.v1 \
    --org org_K11111111111 --workspace t_K11111111111 \
    --from 2026-09-20T12:00:00Z --to 2026-09-21T12:00:00Z \
    --base-url http://127.0.0.1:48253
Error: customer.insights.query was refused (HTTP 503); detail: the ingest side cannot bound what is missing for this window; retry once the frontier has moved; type=coverage_unknown; request_id=req_coverage_unknown_503
```

The command exited with status 1 and printed nothing on stdout, so a script cannot mistake
the refusal for an empty window.

A read context stays valid for five minutes, and any change to the facts it pinned (your
grants, the projection, retention or exclusions) expires it early. When you page through a
drill-down, or pass one overview's `read_context` to a follow-up drill-down so both describe
the same population, a `read_context_expired` means start again from a fresh overview. Do
not splice pages from two contexts into one table.

## 5. Read unknown cost and usage

A missing figure has a reason, and the reason says who can fix it.

| You see | It means | It is not |
|---|---|---|
| receipt `cost.state: unpriced` | the request ran; no price has settled for it yet | $0.00 |
| receipt `cost.state: unavailable`, `reason: scope_not_authorized` | you lack `cost.read`; the figure exists | a failed read |
| cell `unavailable (unpriced_population)` | some rows in the window carry no price, so a sum would understate spend | zero spend |
| receipt `usage: null` with `usage_source: unavailable` | the provider reported no usage, often because nothing was dispatched | zero tokens |
| attempt row `usage_known: false` | this dispatch's token counts are zeros that mean nothing | a free attempt |
| cell `partial` with `ingest_gap:…` | a floor over an incomplete population | the total |

A figure that fills in later is the same request being priced or indexed, not new spend.

## What can go wrong

**You read a window that ends now.** The newest minutes are the ones most likely to be
behind the fence. For a reconciliation, choose a window that ended a while ago and check that
the `gateway_journal` watermark shows no gap.

**You combine a partial count with a complete one.** A floor plus a figure is a floor. Keep
the `coverage` list with any number you copy into a report.

**The data was excluded.** Traffic under a deletion's exclusion leaves every read at once,
and a population where every row is excluded answers `unavailable (excluded)` rather than
zero. [Export or delete data](/docs/build/export-and-delete) covers how exclusion and purge
proceed.

There is no local-log fallback for the hosted service: when the index is behind, the remedy
is to wait for it, not to read a file on the gateway host. This page publishes no indexing
latency bound, because none has been measured and recorded for customers yet.

<NextSteps items={[
  {title: "Trace one request from its ID", href: "/docs/build/observability-request-id", description: "Read the release, attempts and cost state for the request once it is indexed."},
  {title: "Gateway insight reads", href: "/docs/reference/gateway-insights", description: "Every value state, unavailable reason and refusal code, field by field."},
  {title: "Read your gateway's traffic and spend", href: "/docs/build/gateway-insights", description: "Overviews, facets and drill-downs over the window around a request."},
]} />
