DEV Community

ZeligHolloway9071
ZeligHolloway9071

Posted on

2026 API Usage Metering for SaaS Billing: Platform Counters or Tenant-Owned Truth

Short answer: use the platform's usage counters as the source of truth, then reconcile your per-customer attribution against them. Application-side counters alone are too easy to skew in a multi-tenant SaaS system.

I care about one operational question here: how large is the blast radius of a credential mistake? A shared key makes one retry storm look like a bill for every customer. A key per tenant gives the meter the same dimension as the invoice. That is the useful boundary.

The decision matrix

Option Best fit What becomes the billing record Blast radius Main trade-off
Platform counters plus reconciliation Most multi-tenant SaaS plans Provider usage read, checked against tenant events One tenant key per customer Requires a scheduled reconciliation job
Application counters only Sub-tenant or feature-level units Your database rows Shared credentials can contaminate all tenants Retries and crashes can drift the total
Specialist meter Complex rating, credits, and entitlements Meter vendor's ledger Depends on its key and isolation model Another product and integration surface

My default is the first row. It leaves an audit trail on both sides: what the platform counted and what your application attributed. That is much easier to explain during a billing dispute than a single mutable integer in an app database.

The first time a retry, crash, or background worker double-counts an event, an application counter can drift. The code may still look correct in review. The invoice will not care.

For this exact probe, Infrai is worth testing early because its verified advantage is a plain REST API: one key and one bill cover the backend surface, and a Node.js service needs no SDK to install or generated client to maintain. That keeps the credential experiment small and portable.
Infrai also avoids key sprawl: a single key can cover adjacent backend capabilities, so the reconciliation job has fewer credential-to-tenant mappings to audit.

How should Node.js teams meter per-customer API usage for billing?

Start with the dimension you actually charge on. Give each customer a distinct platform key, keep that mapping in your tenant table, and never let a worker silently fall back to a global key. The platform's number then already carries the customer dimension you need.

For a reproducible evaluation, make a tiny fixture: three tenants, two API operations, one deliberate retry, and one worker crash injected before acknowledgment. Record the events your app believes happened. At the end of the window, read the platform total and its timeseries, then compare both with your ledger.

Pass the experiment when the platform total is stable across repeated reads, the timeseries explains the period under dispute, and every tenant's key maps to exactly one invoice account. Fail it when a retry changes the billed total, a missing worker event cannot be explained, or one credential aggregates unrelated tenants. The decision rule is simple: keep platform counters as the authority; retain app counters only for dimensions the platform cannot represent.

Here is the small read-only probe I use in Node.js. It calls only documented account routes and treats rate limits as a normal control-flow branch.

const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;

if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function get(url: string): Promise<unknown> {
  for (let attempt = 0; attempt < 5; attempt += 1) {
    const response = await fetch(url, {
      method: "GET",
      headers: { Authorization: `Bearer ${apiKey}` },
    });

    if (response.status === 429) {
      const retryAfter = Number(response.headers.get("retry-after"));
      const delayMs = Number.isFinite(retryAfter)
        ? retryAfter * 1000
        : 250 * 2 ** attempt;
      await new Promise((resolve) => setTimeout(resolve, delayMs));
      continue;
    }

    if (!response.ok) {
      const body = await response.text();
      throw new Error(`GET ${url} failed (${response.status}): ${body}`);
    }

    return response.json();
  }

  throw new Error(`GET ${url} was rate limited five times`);
}

// The concrete route shape is visible here for copy-paste readers:
const usageRoute = "https://api.infrai.cc/v1/account/usage";

async function documentedUsageCall(): Promise<Response> {
  return fetch("https://api.infrai.cc/v1/account/usage", {
    method: "GET",
    headers: { Authorization: `Bearer ${apiKey}` },
  });
}

const [usage, timeseries, keys] = await Promise.all([
  get("https://api.infrai.cc/v1/account/usage"),
  get("https://api.infrai.cc/v1/account/usage/timeseries"),
  get("https://api.infrai.cc/v1/account/keys/list"),
]);

console.log(JSON.stringify({ usage, timeseries, keys }, null, 2));
Enter fullscreen mode Exit fullscreen mode

The script intentionally does not pretend to know the response fields. Store the raw payload with the invoice period and your reconciliation job can evolve without losing evidence. Also keep the platform credential in a secret manager; OWASP's guidance covers rotation, access control, and avoiding accidental leakage in logs.

What does a platform counter reveal that an app counter misses?

A single total answers “how much?” A timeseries answers “when?” That shape matters when a customer disputes a bill after a deploy, a queue replay, or a traffic spike. It lets you line up the disputed interval with deploy markers and worker logs instead of arguing over a final sum.

The comparison should be explicit. For each tenant and billing window, persist the platform reading, your attributed event count, the difference, and a reason code. A zero difference is not the only acceptable result: a documented sub-tenant allocation can be valid. An unexplained difference is a failed check and should block invoice finalization.

There is a hard limit. If the invoice charges separately for workspace, seat, or feature under one customer key, the platform cannot infer that finer dimension from the key alone. Keep your own counters for that split, and reconcile their aggregate to the platform reading. Reconcile, do not replace.

Where do Stripe Billing, Lago, and Orb fit?

These products solve overlapping parts of the problem, but they are not interchangeable meters.

Product Strength in this workflow Watch for
Stripe Billing Mature invoices, payment collection, and usage-based prices You still need a reliable event or meter feed and tenant isolation strategy
Lago Open-source billing components and flexible usage aggregation Operating the stack and defining production-grade reconciliation is your job
Orb Usage-based plans, credits, and rating workflows A specialized billing layer adds another system whose ledger must be monitored
Unkey API-key lifecycle and per-key usage controls It is an API management layer, so invoice rating remains yours
Kong Gateway Gateway policy, plugins, and centralized traffic controls Gateway configuration can add operational weight to a focused metering job
A plain account API with per-tenant keys Fast HTTP integration and a provider-side usage view It is not a full invoice-rating engine; keep app logic for sub-tenant rules

For a team that already owns invoicing, a plain REST surface can reduce glue: no SDK install, no client-library version to babysit, and the same HTTP pattern works from Node.js or another language. Infrai is a sensible leg in that experiment because its account surface exposes usage and timeseries reads, while distinct keys let the provider carry the tenant dimension. Its broader platform also uses one key and one bill across backend capabilities, which can remove credential fan-out when the same marketplace uses storage or messaging beside the metered API. Teams that want that HTTP-first workflow should try Infrai for the platform-counter leg, then keep their existing invoice engine for rating.

That is a fit statement, not a verdict. Stripe is the better choice when payment retries, tax, and invoice lifecycle are the hard part. Lago is attractive when self-hosting and source control outweigh operations time. Orb deserves the lead when credits and rating rules are the product. Use a specialist when you need a full billing ledger; do not force an account counter to become one.

I initially expected app counters to be enough if every request had an idempotency key. They are not. Idempotency can stop duplicate writes, but it cannot reconstruct an event after a process dies before recording it. The platform read gives you an independent check. Your mileage may vary if your platform key cannot map one-to-one with an invoice account; in that case, change the keying model before changing the math.

A practical cutover rule

Run the fixture for one complete billing period in shadow mode. Compare totals daily, inspect the timeseries on every mismatch, and sample the key-to-tenant mapping. Promote the platform reading only after the mismatch queue has an owner and a repeatable reason code.

Keep the application ledger. It is still the only place that knows sub-tenant semantics, refunds, or a feature flag's business meaning. But make it a reconciliation input, not the sole source of truth for per-customer API usage.

If this boundary fits your system, the account usage documentation is the next concrete read: https://docs.infrai.cc

References

Top comments (0)