You made the pricing decision — usage, or a platform fee plus a meter. Congratulations: you've traded a hard product question for a hard accounting question. Because now you have to count real-world things accurately enough to put a number on an invoice a customer can dispute — and the failure modes here don't crash your app. They quietly overcharge, and you find out in a support ticket that starts with "why was I billed twice."
The mental model that saves you: a meter is an event pipeline, not a counter. You don't increment a number in a database. You emit one event per billable unit, deduplicate it, aggregate the survivors on a schedule, and bill the aggregate. Every hard part lives in that word deduplicate.
The bug that double-charges everyone#
Billing events ride at-least-once infrastructure. An HTTP call times out and your client retries. A queue redelivers a message after a worker restart. A webhook fires twice. This is normal, healthy distributed-systems behavior — and it means the same billable action will arrive more than once. If each arrival mints a new billing event, you count it twice and the customer pays twice.
The fix is a deterministic idempotency key derived from the action itself — a request id, a resolved-ticket id, a transaction id. The one thing it must never be is a value generated at send time:
// WRONG — a "unique" id minted on each send. Every retry looks new.
const identifier = crypto.randomUUID(); // ❌ fresh per attempt
// WRONG — timestamps collide or diverge, neither is stable.
const identifier = `bill-${Date.now()}`; // ❌ not tied to the action
// RIGHT — derived from the real-world event. Retries collapse to one.
const identifier = `resolution:${ticket.id}`; // ✅ stable across retries
Same action, same identifier, every time — so the retry that was supposed to guarantee delivery doesn't also guarantee a double charge.
The fast path: Stripe Billing Meters#
If Stripe already runs your billing, you don't build the pipeline — you send events into theirs. The modern Meter Events API replaces the legacy usage-records system:
await stripe.billing.meterEvents.create({
event_name: "ticket_resolution", // matches a Meter you defined
payload: {
stripe_customer_id: customer.id,
value: "1", // one resolved ticket
},
identifier: `resolution:${ticket.id}`, // Stripe dedupes on this
});
Two details do the heavy lifting. The identifier is deduplicated by Stripe over a rolling window of at least 24 hours, so a retry storm inside a day collapses to a single billed unit for free. And value should be denominated in exactly the unit you price on — send 1 per resolution if you charge per resolution; send a token count if you charge per 1K tokens and let the meter's aggregation divide. Stripe rolls the events up (sum, count, or last) and bills them on the subscription. timestamp defaults to now and must fall within the past 35 days, which matters only if you're backfilling.
One rule that outranks all of this: emit the event server-side, at the moment the work is confirmed done — the resolved ticket closed, the API response flushed — never client-side and never at request time. Client metering is spoofable and fires before you know the action succeeded, which is precisely wrong for outcome pricing, where you only bill for wins.
When to reach for a dedicated meter#
Stripe's aggregation is deliberately simple. The moment you need prepaid credit grants, running balances you enforce in real time, unique-count metering, or a warehouse-grade event trail for finance to audit, you want a purpose-built meter:
These sit between your app and Stripe (or your invoicing system): they ingest raw events, own the dedup and aggregation, and hand a clean total downstream. You still owe them a stable identifier — the double-charge bug is yours to prevent no matter whose pipeline you rent.
The checklist#
Before you bill a single customer on usage, verify four things end to end. One: every billable action emits exactly one event, server-side, after the work is confirmed. Two: each event carries a deterministic identifier derived from the action, and you've tested that replaying an event twice charges once. Three: the value is denominated in your pricing unit and nothing else, so a price change is a config edit, not a data migration. Four: you can reconstruct any invoice line from the underlying events — because the first serious dispute will ask you to, and "trust our counter" is not an answer. Get metering right and usage pricing is a spreadsheet; get it wrong and it's a refund queue with your name on it. For the cost side of the same ledger — knowing what each customer costs you before you decide what to charge — see how to track LLM cost per customer.



