Skip to content

Usage & metering

Metric Basis Billed
Storage physical bytes — post-dedup, post-compression managed storage only
Egress bytes served on download authorization, per calendar month managed storage only
Objects, operations counted for reporting no
BYOS storage and egress reported, split from managed no — your provider bills you

Two things follow from “physical, not logical”:

  • Deduplication is a direct discount. Better dedup means a smaller bill, not just a nicer number in the dashboard.
  • Logical size is not your bill. A repository showing 500 GB logical and 60 GB physical costs 60 GB.

See Deduplication explained.

Two independent mechanisms, which is deliberate — one is precise, the other is the safety net.

Finalizing a push publishes repository.usage.changed. The worker then:

  1. Sums the real stored bytes under the repository’s storage namespace, straight from the object store.
  2. Writes the daily usage snapshot that GET …/usage and the dashboard read.
  3. Evaluates the plan’s thresholds and emits quota warnings.

Latency is seconds. If the object store is briefly unreachable it falls back to a conservative estimate and retries — so a figure can be approximate for a short window, never silently wrong for long.

Object count and generation update immediately at finalize; byte totals follow.

Every metered operation also appends an immutable ledger entry — the authoritative per-event record that billing, quotas and margin analysis read:

Field Meaning
timestamp When
organization_id, project_id Who and where (project_id is the repository)
user_id The acting user, when there is one
operation upload, download, finalize, delete
logical_bytes, physical_bytes, deduplicated_bytes Sizes
uploaded_bytes, downloaded_bytes Transfer direction
object_count, operations Counts
storage_class managed or byos
plan, region Context at the time

Ledger appends are best-effort from the request path — a failure is logged and never blocks your push. Under normal operation the ledger is complete, with the daily rollup as a cross-check.

Terminal window
export API=https://cavsnode.com/api/v1
export H="Authorization: Bearer $CAVS_TOKEN"
curl -sS "$API/organizations/acme-ai/usage" -H "$H"
curl -sS "$API/organizations/acme-ai/usage/series?days=90" -H "$H"
curl -sS "$API/organizations/acme-ai/dashboard" -H "$H"
curl -sS "$API/repositories/$REPO/usage" -H "$H"
curl -sS "$API/repositories/$REPO/storage/series?days=90" -H "$H"
Terminal window
cav storage
cav storage --json | jq '.saved_pct'

Permission: usage.read — every role except… none, actually. All five roles have it, including VIEWER and BILLING. Token scopes usage:read or org:read also reach it.

usage splits managed from BYOS bytes, since only managed is metered.

The organization dashboard projects storage and egress to the end of the billing period from the current trend, alongside the plan’s included amounts and the overage that would result.

Terminal window
curl -sS "$API/organizations/acme-ai/dashboard" -H "$H" | jq '.forecast'

It’s a linear projection, not a promise. Treat a forecast crossing your limit as a prompt to look at the folder breakdown, not as a bill.

Plan Storage / GB-month Egress / GB
Developer $0.10 $0.05
Team $0.10 $0.05
Business $0.09 $0.04
Free none — hard limits none

Computed at micro-cent precision (1 cent = 1,000,000 micro-cents), so small amounts are billed exactly rather than rounded up.

Nothing is cut off on a paid plan — you’re billed for what you use. On Free, the limits are hard and an over-limit upload is refused with 402.

Event Effect
storage.quota.warning Notification + email + webhook. Nothing blocked.
storage.quota.exceeded Notification + email + webhook. Organization marked PAST_DUE.

Storage quota is checked before upload authorization, using logical sizes as an upper bound, so an overflowing batch is refused up front rather than half-written.

In rough order of effect:

  1. Use the CAVS transfer agent. Without it there’s no chunking, and a re-pushed file costs full price every time.
  2. Stop shipping solid archives. .tar.gz and .tar.zst re-encode the whole stream on any change. A plain .tar gets the same compression from CAVS and near-total reuse on subsequent pushes. This is usually the single biggest win.
  3. Delete stale branches. GET …/branches flags branches with no commits in 30 days and branches over 2× the default branch’s footprint — a long-lived experiment branch pinning a dozen checkpoints is a common surprise.
  4. Find the actual culprit. GET …/storage/folders?depth=3, then GET …/objects?sort=size. Guessing wastes time.
  5. Cut needless egress. Fetch selectively (git lfs pull --include=…); don’t re-download in CI what a cache already holds; don’t pre-authorize downloads speculatively — metering happens at authorization, not at transfer.
  6. Read the recommendations. GET …/recommendations — CAVS may have already spotted it.
  7. Consider BYOS. If you already have committed storage capacity or negotiated rates, moving bytes to your own bucket removes them from CAVS metering entirely.
Symptom Explanation
Usage looks stale right after a push Reconciliation lags by seconds. Object count and generation update immediately.
Physical ≈ logical Nothing deduped. Almost always: the agent isn’t wired, or the format is a solid archive.
Egress higher than expected Download authorizations are metered, not completed transfers.
Storage didn’t drop after deleting a branch GC does not yet reclaim unreferenced chunks — see Lifecycle.
BYOS repository shows bytes but no charge Correct. BYOS is reported, not billed.
Summing per-object physical_size ≠ repository physical Expected — shared chunks would be double-counted.