How to Implement Usage-Based Billing in Your SaaS

Usage-based billing charges customers for what they actually consume — and the accuracy of your metering is what makes or breaks the entire model. Pick the wrong unit, skip idempotent writes, and you'll either double-charge customers or quietly give away usage for free. Here's the implementation that prevents both outcomes.
Quick Answer
Usage-based billing charges customers for what they actually use. To build it, pick the unit you meter (API calls, credits, seats), record each usage event once (idempotently, so retries do not double-count), aggregate the total per billing cycle, report it to your payment provider, and reset the counter when the cycle renews. The accuracy of your metering — not the charge itself — is what makes or breaks it.
What usage-based billing is
Usage-based billing charges customers in proportion to how much of a product they consume. It contrasts with flat subscription pricing, where everyone on a plan pays the same. It is one of the core models covered in the SaaS billing guide.
When to use it
Use usage-based billing when value scales with consumption — API platforms, messaging, compute, AI tokens. Use flat subscription billing when value is roughly constant per user. Many products combine a base subscription with metered overage.
Step 1: Define the metered unit
Pick one clear, countable unit your customer understands: "1 credit per API call," "per 1,000 emails sent," "per active seat." Ambiguous units cause billing disputes.
Step 2: Record usage events idempotently
Every billable action emits an event. The critical rule: record each event once, even if the request is retried. Use a unique event id. (Untested-here: illustrative.)
export async function recordUsage(
customerId: string,
eventId: string,
qty = 1
) {
// unique constraint on eventId makes retries a no-op
await db.usageEvent.upsert({
where: { eventId },
create: { customerId, eventId, qty, ts: new Date() },
update: {}, // already recorded -> do nothing
});
}Step 3: Aggregate and report per cycle
At the end of each billing cycle (or continuously), sum the events and report the total to your payment provider's metered/usage API.
const total = await db.usageEvent.aggregate({
where: { customerId, ts: { gte: cycleStart, lt: cycleEnd } },
_sum: { qty: true },
});
await provider.reportUsage(customerId, total._sum.qty ?? 0);Step 4: Reset per cycle
When the billing cycle renews, start a fresh count for the new period. Keep the historical events for audit and dispute resolution; just scope aggregation to the current cycle.
Common issues
Double-counting on retries
The number one usage-billing bug. Always key events on a unique id and make the write idempotent (Step 2).
Drift between your count and the provider's
Report on a schedule and reconcile. Treat your database as the source of truth and the provider as the invoicing layer.
How FastStaq helps
FastStaq includes a credits model (CreditBalance / CreditTransaction) with per-cycle reset logic and a shared, idempotent provisioning path — which is exactly the machinery usage-based billing needs: a place to record and total consumption and a safe, retry-proof way to grant and charge. You build on that instead of inventing a metering store from scratch.


