PerdurancePerdurance

Usage and storage

What a namespace has spent per key, what it is holding, and why accepted submissions and provider calls are different numbers.

Two routes, both scoped to a namespace and both needing a key that can manage it.

GET /usage

What the namespace has spent, in total and attributed per key.

curl "$PERDURANCE_URL/usage" -H "Authorization: Bearer $PERDURANCE_KEY"
{
  "namespace_id": "…",
  "itemised_since": 1787900000000,
  "accepted_submissions": 1841,
  "provider_calls": 1792,
  "upstream_sent_bytes": 4192048,
  "upstream_received_bytes": 19402881,
  "api_keys": [
    {
      "api_key_id": "…",
      "ledger_events": 1204,
      "provider_calls": 1180,
      "prompt_tokens": 884102,
      "completion_tokens": 219884,
      "total_tokens": 1103986
    }
  ]
}

accepted_submissions and provider_calls are deliberately different numbers. A submission that landed on an existing execution inside the idempotency window is counted once as a submission and not at all as a provider call. The gap between them is what deduplication saved you.

The totals above api_keys are all-time. The itemisation is not: it covers the last thirty days by default, and itemised_since echoes back the start of the window that was used, in epoch milliseconds, rather than the oldest event found in it. ?since=<epoch millis> widens or narrows the window:

curl "$PERDURANCE_URL/usage?since=1780000000000" -H "Authorization: Bearer $PERDURANCE_KEY"

The window is bounded by default because the itemisation aggregates the request history, which grows with the age of the namespace. The namespace-wide figures are a rollup and cost the same whatever the window is.

A token count is null where no answer under that key reported one, which is not the same as a provider reporting zero. Treat null as "not known", not as "none".

Attribution

Every figure in api_keys is per key, which is what makes a key the unit of cost accounting. Issue one per service, per environment, or per customer, and this route tells you what each of them spent without a second system.

There is no per-model breakdown here. Per-model detail is on the listing: every row of GET /requests carries the model it ran.

GET /storage

What the namespace is currently holding, and where it went.

curl "$PERDURANCE_URL/storage" -H "Authorization: Bearer $PERDURANCE_KEY"
{
  "namespace_id": "…",
  "stored_bytes": 20418302,
  "request_bytes": 4192048,
  "response_bytes": 2884102,
  "chunk_bytes": 13342152
}

stored_bytes is the sum of the other three. The split matters because they grow for different reasons: request_bytes tracks how much you send, response_bytes how much comes back whole, and chunk_bytes how much comes back streamed — which is usually the largest, because a streamed answer is stored as its individual frames so it can be replayed and resumed.

Keeping it down

Retention is a namespace setting, in minutes, and it is unset by default. Set it in the console under Namespaces, or with PATCH /{tenancy}/v1/namespaces/{namespace}. A namespace that names no horizon takes its tenancy's; a namespace that names 0 asks to keep everything even where its tenancy sweeps.

There is a ceiling on that ask. A record cannot outlive the deployment's metadata horizon, which is three months by default, because the metadata sweep deletes the request row and the payload goes with it. The shorter of the two wins. That is a silent clamp rather than a refused write, so 0 means "as long as the deployment keeps the metadata" rather than "for ever". You do not have to work the rule out. PATCH /{tenancy}/v1/namespaces/{namespace} answers with effective_retention_minutes, which is the figure the sweep will apply.

What a sweep takes is the record: the request body, the answer, and the chunks. What it leaves is everything you bill and report from — the status, the model, the token counts, the cost, the timings, and the fee ledger. Those have their own, much longer horizons, set for the whole deployment rather than per namespace.

So shortening a namespace's retention bounds your disk without costing you the ability to reconstruct an invoice. Retrieving a swept record answers 410 Gone rather than 404 Not Found, because the request did happen and everything about what it did is still there.

Lapsed idempotency claims are swept whatever retention says, because a claim is not a record.

Per-request sizes are also in the listing: GET /requests gives stored_bytes per row, which is how you find the handful of requests holding most of it.

On this page