DEV Community

FlorianBlake3536
FlorianBlake3536

Posted on

API Usage Metering for SaaS Billing: Node.js Source-of-Truth Counters Explained

Short answer: for a fintech SaaS invoice, keep an append-only usage ledger as the source of truth and use platform counters as a fast cross-check, while isolating each tenant's credential so one leak cannot rewrite everyone else's bill.

The bill is made of requests, not dashboards. Before choosing a counter, write the equation your finance team will defend: billable units = accepted requests - explicitly refunded requests, grouped by tenant, product, and billing window. In most systems the dominant term is the number of accepted events and the retention needed to prove them; a tiny dashboard query is rarely the expensive part.

That framing changes the design. Store an immutable event with an idempotency key, tenant identifier, meter name, quantity, and event time. Keep a compact daily rollup for reads. Stop retaining raw payloads once the audit window and dispute policy allow it; retaining less lowers storage and privacy exposure, but a missing payload makes a disputed invoice harder to reconstruct.

What should be the source of truth for per-customer API usage metering?

There are three useful layers, and they answer different questions. The request gateway knows what was accepted. A platform counter can answer “how many units have accumulated?” quickly. Your ledger answers “which signed event produced this number, and can I replay it?” Treating any one layer as all three creates a failure mode.

Layer Good at Failure to design around
Gateway event log Attribution and replay Duplicate delivery or clock skew
Platform counter Low-latency balance checks Limited detail and provider-specific retention
Tenant-owned ledger Audits, corrections, and exports You own compaction, access control, and recovery

I prefer the ledger to be authoritative for invoices, with a counter as a derived projection. A retry then appends the same idempotency key instead of silently adding a second unit. If a projection falls behind, billing can wait or rebuild it; if the ledger is wrong, a pretty counter only hides the problem.

How can a Node.js metering path limit the blast radius of one credential?

Start with identity boundaries, not rate limits. Give each tenant a scoped credential, store only a reference to it, and make the service that accepts usage unable to read another tenant's events by default. OWASP's secrets guidance recommends controlled access, rotation, and audit trails; those are billing controls because a stolen key can manufacture billable activity.

The following Python sketch shows the important ordering. The API handler authenticates, derives the tenant from the credential, and writes an idempotent event before updating a read model. The real service can be Node.js; the snippet stays in Python so the storage contract is visible without tying the decision to a framework.

from dataclasses import dataclass
from hashlib import sha256

@dataclass(frozen=True)
class UsageEvent:
    tenant_id: str
    meter: str
    quantity: int
    request_id: str

def record_usage(store, credential, meter, quantity, request_id):
    tenant_id = store.tenant_for_credential(credential)
    if tenant_id is None or quantity <= 0:
        raise ValueError("reject unscoped or non-positive usage")

    event_id = sha256(f"{tenant_id}:{request_id}".encode()).hexdigest()
    event = UsageEvent(tenant_id, meter, quantity, request_id)
    inserted = store.insert_event_once(event_id, event)
    if inserted:
        store.increment_rollup(tenant_id, meter, quantity)
    return store.current_total(tenant_id, meter)
Enter fullscreen mode Exit fullscreen mode

The critical property is insert_event_once, backed by a unique constraint on (tenant_id, request_id). A timeout after the insert is not proof that the event failed; the caller must retry safely. I once assumed a counter increment was enough, then found that a replay after a network timeout inflated one customer's usage by 2 units. The fix was boring: make the event key explicit and inspect the ledger during reconciliation.

When do platform counters beat your own counters for metered billing?

Use a managed counter when the question is operational and disposable: “may this tenant send another request in this minute?” It is a good admission-control signal, especially when a refused request is safer than an unbounded spend. Keep the invoice path on your ledger when a customer can challenge a charge, when corrections need a reason code, or when retention and deletion rules differ by tenant.

The trade-off is not vendor prestige; it is recovery scope. A platform counter reduces code and on-call work, but its window semantics, export shape, and retention become part of your accounting contract. An own counter gives you replay and migration freedom, at the cost of backups, compaction, and a documented rebuild procedure.

The catch is that a ledger-first design is not suitable when you cannot operate durable storage or protect sensitive tenant identifiers. Stick with a platform counter for a short-lived quota, or choose a database service your team already recovers confidently. Your mileage may vary when the invoice window crosses time zones; pin the window to UTC and record the rule beside each rollup.

A reconciliation loop that survives outages and corrections

Do not “fix” a mismatch by editing a total. Compare the platform reading with a replayed ledger window, emit a signed adjustment event, and preserve both the original and corrected values. During an outage, queue accepted events at the edge with bounded retention and an idempotency key; refuse traffic once that queue would exceed the spend ceiling rather than pretending the meter is current.

The reconciliation job deserves the same design attention as the request path. Give it a watermark per tenant, because one noisy customer should not hold the whole batch hostage. Read events in a stable (event_time, event_id) order, verify the credential scope recorded with each event, and write a checkpoint only after the derived total is durable. If a job dies after writing a rollup but before its checkpoint, replaying the window must produce the same total; that is why the rollup update and checkpoint belong in one transaction where the database supports it. For cross-region storage, record which region accepted the event and use a deterministic conflict rule rather than whichever replica answered last. A correction is a new event with an operator, reason, and timestamp, never a mutable overwrite. This may feel fussy for a small account, but a fintech invoice turns a harmless-looking counter into evidence that can be exported, challenged, and reviewed by someone who was not on call. Your incident runbook should include a dry-run replay against a copy of the ledger, a maximum adjustment threshold that requires a second approver, and a clear decision for events that arrive outside the billing window.

Keep it boring.

Observability should expose age of the newest ledger event, projection lag, duplicate-event rate, and the count of manual adjustments. Alert on a tenant-specific deviation, not only a global average. A single leaked credential may affect one account while the fleet looks healthy.

Decision rule for a defensible invoice

Choose the smallest system that can answer these four questions six months later: who generated the usage, which credential authorized it, why was the quantity accepted, and how was a correction approved? For a low-stakes quota, a platform counter plus a short audit log is enough. For a metered fintech invoice, an append-only ledger, derived counters, scoped credentials, and replayable reconciliation are the safer boundary.

That choice deliberately stops keeping raw request bodies after the agreed audit period. It saves retention and reduces exposure, but it means a dispute must rely on event metadata and signed adjustments. Write that limitation into the billing policy before launch.

References

Further reading

Top comments (0)