---
slug: vectors/usage-and-metrics
title: Usage and metrics
kind: reference
surface: vectors
summary: The two Aetherfy analytics reads — plan usage and measured request telemetry — including the histogram-bound percentiles, the coverage rule that separates a real zero from an unobserved one, and the null sentinels both endpoints use.
sources:
  - vectordb:backend/routes/analytics.js
  - vectordb:backend/services/usageMetrics.js
  - docs/TELEMETRY.md
---

# Usage and metrics in the Aetherfy vector API

The Aetherfy vector API answers two analytics reads. `GET /api/v1/analytics/usage`
reports what you are storing against your plan. `GET /api/v1/analytics/metrics`
reports request telemetry that Aetherfy actually measured. Both take the same
Bearer API key as every other data-plane call.

Read the two rules below before you build anything on `/metrics`. They are the
difference between a dashboard that reports what happened and one that quietly
invents it.

## The Aetherfy zero rule: null is not zero

An empty counter result means one of two very different things — you made no
requests, or nothing was counting. Aetherfy separates them rather than letting
you guess.

A producer writes a heartbeat row on every flush, whether or not there was
traffic. `coverage.fresh` reports whether the collectors behind your window are all
reporting recently.

| `coverage.fresh` | What the numbers mean |
| --- | --- |
| `true` | Attested. A `0` is a measured zero: that thing did not happen |
| `false` | **Not attested.** Every value is unknown, including the ones that look like numbers |

When `coverage.fresh` is `false` the Aetherfy API sets every field in `totals`
to `null` and returns empty arrays — it does not return zeros. A client must
render that state as no-data. Showing `0 requests` for an unobserved window is
the exact failure this endpoint was built to avoid, and it is indistinguishable
from a genuinely quiet week to everyone except the person paying for it.

`coverage.regions` carries per-region freshness, so a client that wants to say
"two of three regions reporting" can. The top-level flag is an AND across all of
them: one dark region must not let its absent rows render as real zeros.

A failed read is an error response, never a body of zeros.

## Aetherfy percentiles are histogram upper bounds, not interpolations

`p50_ms` and `p95_ms` are **not** interpolated percentiles. Aetherfy keeps eight
fixed latency buckets, and the reported figure is the smallest bucket bound whose
cumulative count reaches the rank.

The bounds are 5, 10, 25, 50, 100, 250 and 1000 milliseconds, plus an unbounded
bucket above 1000.

So `p95_ms: 100` means **at least 95% of requests finished within 100 ms**. It
does not mean the 95th percentile was 100 ms. Treat every value as an upper
bound and never subtract two of them to claim a change.

When the rank falls into the unbounded bucket above 1000 ms there is no honest
bound to report, and Aetherfy returns `null` rather than inventing one. That is
what `max_ms` is for: it is exact, not a bound, and it is the number to read when
a percentile is null.

`max_ms` is also `null` when nothing was measured. With a fresh heartbeat and
zero requests, the counts are `0` and all three latency fields are `null` —
there is no p50 of nothing, and `0 ms` would be a fabrication.

## Aetherfy measures latency inside the API process

Every duration these endpoints report is measured server-side, inside the
Aetherfy API process. It excludes your network path, TLS negotiation, and any
time spent in your own client.

The latency your users experience is therefore always larger than what
`/metrics` reports, and the gap is mostly geography. If you need the number your
application actually feels, time your own calls at your own call site — that is
the only figure that includes everything between you and Aetherfy.

## Reading Aetherfy plan usage: GET /api/v1/analytics/usage

Returns storage and collection usage against your plan. No parameters.

| Field | Type | Meaning |
| --- | --- | --- |
| `storage_bytes_used` | integer | Bytes stored across every active collection |
| `storage_limit_bytes` | integer or `null` | Plan storage limit. `null` means unlimited |
| `collections_count` | integer | Active collections |
| `collections_limit` | integer or `null` | Plan collection limit. `null` means unlimited |
| `tier` | string | Your plan |
| `active_regions` | string array | The regions your collections occupy |
| `usage_percentage` | integer | Storage used as a percentage of the limit; `0` when there is no limit |

**Both limit fields use `null` for unlimited.** Not `0`, not `-1`, not a string
— one sentinel, the same for each. A client that treats `null` as `0` will show
an account on an unlimited plan as being permanently over quota.

`active_regions` is the union of every active collection's regions, so it
reflects where your data actually is. Region placement is plan-scoped in
Aetherfy: Free and Starter collections occupy a single region, and replication
across more than one region begins at the plan named Performance. On a
single-region plan this array holds exactly one entry, and it is empty when the
account has no active collections.

```bash
curl -s https://vectors.aetherfy.com/api/v1/analytics/usage \
  -H "Authorization: Bearer $AETHERFY_API_KEY"
```

## Reading Aetherfy request telemetry: GET /api/v1/analytics/metrics

Returns measured request telemetry for your account. Every number is a sum over
counter rows written after observing a real response.

`window` accepts `24h` (default), `7d` or `30d`. The first two bucket by hour;
`30d` buckets by UTC day, because 720 hourly points is a chart nobody can read.
Any other value returns `400`, and the Aetherfy error body names the offending
parameter and the accepted set:

```json
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "window must be one of 24h, 7d, 30d",
    "field": "window",
    "allowed": ["24h", "7d", "30d"]
  }
}
```

`field` and `allowed` sit directly on the error object, so a client can branch on
them without parsing the message.

The response carries seven top-level keys.

| Key | Contents |
| --- | --- |
| `window` | The resolved `name`, `from`, `to` and `bucket`, from one server clock read |
| `coverage` | `fresh`, `stale_after_seconds`, `last_flush_at`, `last_record_at`, `regions[]` |
| `totals` | `requests`, `errors_4xx`, `errors_429`, `errors_5xx`, `p50_ms`, `p95_ms`, `max_ms` |
| `series` | One entry per bucket: `bucket_start`, the four counts, and `p95_ms` |
| `by_op` | Per operation: `search`, `upsert`, `retrieve`, `delete`, `collections`, `other` |
| `by_collection` | Top 25 collections by requests; requests carrying no collection are excluded |
| `by_region` | The Aetherfy region that served each request |

`window` is echoed back rather than left for you to re-derive. A client
recomputing the boundaries from its own clock would disagree with the server by
its own skew plus the request's flight time, and would have no way to tell that
apart from a real gap in the data.

Every field in `totals` is nullable, and `null` never means zero — see the zero
rule above.

```bash
curl -s "https://vectors.aetherfy.com/api/v1/analytics/metrics?window=7d" \
  -H "Authorization: Bearer $AETHERFY_API_KEY"
```

## Reading Aetherfy usage from the SDKs

Both Aetherfy SDKs expose the usage endpoint as a method. Neither wraps
`/metrics` — call it over REST.

The full field list and the SDKs' shared return shape are on the
[SDK reference](/vectors/sdk). The machine-readable contract for both endpoints
is in [the OpenAPI index](/openapi.json), and the error codes are on the
[errors page](/vectors/errors).
