---
title: "Meter usage per tenant"
description: "Record what each tenant uses, read it back for a usage page, count a retry once and send hourly totals to your billing provider."
section: "guides"
status: "Building for 0.3"
spec: "meter"
canonical: "https://mainsdk.dev/docs/guides/meter-usage-per-tenant"
updated: "2026-10-28"
---

# Meter usage per tenant

> Record what each tenant uses, read it back for a usage page, count a retry once and send hourly totals to your billing provider.

Status: Building for 0.3 · Canonical: https://mainsdk.dev/docs/guides/meter-usage-per-tenant · Updated: 2026-10-28

You will record API calls per tenant, show today's total and make a retried step count once. It takes about 15 minutes.

## You need

- A MainSDK app with `tenancy` (see [build a multi-tenant app](https://mainsdk.dev/docs/guides/build-a-multi-tenant-app)).

## 1. Declare the metric

```yaml title="main.yaml"
meter:
  driver: local
  metrics:
    api_calls: { unit: call, aggregate: sum }
```

`local` is for development. In production use `db` or `redis`, or `mainsdk check` reports `MAIN-METER-0006`.

## 2. Record after the work succeeds

```ts title="lib/usage.ts"
import { meter } from "mainsdk";

meter.record("api_calls", 1, { feature: "export" });   // returns { id, queued: true } at once
```

`record` never throws because the store is down: the record is buffered.

## 3. Read usage

```ts title="app/usage/page.tsx"
import { meter } from "mainsdk";

const usage = await meter.usage({ metric: "api_calls", window: "month", groupBy: ["feature"] });
for (const g of usage.groups ?? []) console.log(g.key.feature, g.value);
```

## 4. Count a retry once

```ts title="lib/transcribe.ts"
import { meter } from "mainsdk";

export function countMinutes(jobId: string, minutes: number) {
  meter.record("audio_minutes", minutes, { idempotencyKey: `transcribe:${jobId}` });
}
```

Inside a workflow step, job or inbound webhook the key defaults from that context.

## 5. Test it

```ts title="lib/usage.test.ts"
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { meter } from "mainsdk";

it("counts a retried record once", async () => {
  createTestApp({ main: "./main.yaml" });
  meter.record("api_calls", 1, { idempotencyKey: "req-1" });
  meter.record("api_calls", 1, { idempotencyKey: "req-1" });
  await meter.flush();
  assert.equal((await meter.usage({ metric: "api_calls", window: "day" })).value, 1);
});
```

## Check it worked

The test passes with a value of 1. In the app, `usage.stale` is false when the store answered.

## Troubleshooting

- `MAIN-METER-0001`: the metric is not under `meter.metrics`. Declare it.
- `MAIN-METER-0004`: usage came from the last known value because the store is unreachable (`stale: true`).
- `MAIN-METER-0007`: no store for the tenant's region. Add `meter.redis.<region>`.

To bill by usage, see [take a payment](https://mainsdk.dev/docs/guides/take-a-payment) and the [meter interface](https://mainsdk.dev/docs/interfaces/meter) for `meter.export`.
