Idempotent Usage Metering: Deduplicating Events and Handling Late Arrivals Without Double-Charging

A usage meter that bills customers must count each event exactly once even when delivered twice, and still get the count right when events arrive late or out of order. How to do it with a stable per-event identifier, a dedup window, and an acceptance window, with the documented behavior of Stripe meter events, OpenMeter, and Lago.

9 min read

usage meteringidempotencydeduplicationmetered billingStripe meter eventslate eventsingestionusage-based pricing

The short version: A usage meter that bills customers needs two properties that ordinary event logging does not: it must count each billable event exactly once even when your pipeline delivers it twice, and it must still get the count right when events show up late or out of order. You get the first with a stable per-event identifier and a deduplication window. You get the second with an acceptance window that stays open until just before the invoice is cut. Skip either and you ship invoices that are wrong in a way customers notice.

Why metering is not just event logging

If you have ever shipped an analytics pipeline, the instinct is to treat usage events the same way: fire and forget, accept some loss, accept some duplication, smooth it out in the dashboard later. That is fine for a funnel chart. It is not fine for an invoice. A duplicated event in analytics nudges a number. A duplicated event in a meter charges a customer twice, and the customer who notices a double charge is the customer who churns and tells their network why.

The two failure modes are symmetric and both are common:

  • Over-counting from at-least-once delivery. Your client retries on a timeout, your queue redelivers on a consumer crash, your webhook fires twice. The same billable action lands in the meter more than once.
  • Under-counting from dropped or late events that miss the billing cut-off. An event that arrives after the invoice is finalized either never gets billed or gets shoved into next month against the wrong meter.

The whole discipline of metering ingestion is making over-counting impossible and under-counting recoverable. Everything below is in service of those two goals.

Idempotency: count each event exactly once

The mechanism is simple and every serious metering system implements a version of it: attach a stable, unique identifier to each billable event, and have the ingestion layer reject or fold any event whose identifier it has already seen inside a defined window.

How the major systems name it

SystemField that dedupsDedup window
Stripe Billing meter events (v2)identifier (generated if omitted)Rolling period of at least 24 hours
OpenMeterCloudEvents id + source32 days by default
Lagotransaction_idPer the event pipeline; pairs with timestamp on the high-volume path

Stripe documents the behavior plainly: the identifier on a meter event is "a unique identifier for the event. If not provided, one is generated. We recommend using UUID-like identifiers. Stripe enforces uniqueness within a rolling period of at least 24 hours." OpenMeter takes the CloudEvents position that "producers MUST ensure that source + id is unique for each distinct event," and dedups on that pair. Lago requires a unique transaction_id per event so the same event cannot be ingested twice. Different names, identical idea.

Construct the identifier so it survives a retry

The identifier only works if the same logical event carries the same identifier across retries. That is the part teams get wrong. A few rules that hold up in production:

  • Generate the identifier at the source of the billable action, not at send time. If you mint a fresh UUID inside the HTTP client's retry loop, every retry gets a new identifier and the dedup window never catches it. Mint it once where the action happens, then attach it to every send attempt.
  • Make it deterministic where you can. If the billable unit already has a natural key (a request ID, a job ID, an inference call ID), hash that into the identifier. OpenMeter's own guidance is to combine random and time components with source information to keep collision risk low while staying reproducible.
  • Do not encode mutable state into it. The identifier must be stable even if the event's payload is later corrected, otherwise a correction looks like a brand-new billable event.

Prefer at-least-once delivery plus dedup over trying for exactly-once delivery

Exactly-once delivery across a network is famously hard and usually a trap. The pattern that works is at-least-once delivery with exactly-once processing: let the client retry freely, let the queue redeliver, and lean on the identifier to fold the duplicates at the meter. OpenMeter states the trade-off directly: "It's better to report a usage twice and filter out duplicates later than to underreport it." Build your client to retry aggressively and your ingestion to dedup ruthlessly. That combination is both simpler to operate and safer for the bill than chasing delivery guarantees you cannot get.

Late and out-of-order events: get the count right anyway

Dedup solves over-counting. It does nothing for the event that took the scenic route and arrived four hours after it happened, possibly after a faster event that happened later. Distributed producers, mobile clients with intermittent connectivity, and queue backlogs all generate late and out-of-order events as a matter of course. A meter has to absorb them.

Carry an event-time timestamp, and respect the backdating limit

Every event needs its own timestamp recorded at the moment the billable action occurred, not the moment the meter received it. Bill against event time, not ingestion time, or your monthly boundaries will smear usage into the wrong period. Stripe enforces this with a hard rule on meter events: the timestamp "must be within the past 35 calendar days or up to 5 minutes in the future," and defaults to the current time if omitted. Anything older than 35 days is rejected. Two practical consequences:

  • If you batch or buffer events on a client that can be offline for a long time, you can blow past the 35-day window and have events silently rejected. Flush more often than that, with margin.
  • The five-minutes-into-the-future allowance exists for clock skew. If your producers' clocks drift more than that, valid events get rejected. Run NTP and treat clock skew as a billing bug, not an ops nicety.

Keep an acceptance window open until the invoice is cut

The fix for late arrivals is to not finalize usage the instant a billing period closes. Hold an acceptance window, typically somewhere in the 24 to 72 hour range, during which late events for the just-closed period are still folded into that period's totals before the invoice is finalized. This is the single most effective guard against under-billing, and it costs you only a short delay between period close and invoice generation.

Stripe's own asynchronous processing reflects the same reality: meter events are validated synchronously but processed asynchronously, and there is an informal grace window of roughly an hour for late events to land before aggregation. Their documentation is explicit that this is not a guarantee you should depend on, which is exactly why you should run your own acceptance window with a known, owned duration rather than relying on someone else's undocumented one.

Have a backfill path for events that arrive after finalization

No acceptance window is long enough to catch everything. Eventually an event shows up after the invoice is already cut. You need a deliberate answer for that case rather than letting it fall on the floor:

  • Roll it forward. Apply the stray event to the current open period. Simple, but it bills the customer in the wrong month, which matters for anyone doing revenue recognition or chargeback by period.
  • Issue a correction. Re-open the closed period's aggregate and produce an adjustment or credit note. More accurate, more machinery. OpenMeter, for instance, exposes backfill APIs specifically to correct historical usage after the fact.

Pick one, write it down, and make it the same answer every time. The failure mode here is not picking either, so late events get handled ad hoc by whoever is on call that day.

Know what your meter can and cannot aggregate

Idempotency and late handling get the raw event set right. Aggregation turns that set into a billable number, and the aggregation function you have available constrains what pricing you can express. Stripe meters, as one concrete example, support sum, count, and last aggregations only. There is no built-in max, min, or avg. If your pricing needs "peak concurrent seats this month" or "average GB stored per day," you compute that yourself upstream and report the resolved number as a last value on a meter. Check your platform's supported aggregations before you promise a pricing model to customers, because retrofitting an unsupported aggregation after launch means re-architecting ingestion.

A minimal correctness checklist

If you are building or auditing a usage meter, these are the properties to verify. Each one maps to a real way invoices go wrong.

PropertyPrevents
Stable per-event identifier minted at the action sourceDouble-charging from client/queue/webhook retries
Dedup window at least as long as your worst-case retry horizonDuplicates slipping past the window and getting billed
Event-time timestamp carried on every eventUsage smeared into the wrong billing period
Producers within the platform's backdating limit (e.g. 35 days for Stripe)Old buffered events being silently rejected
Acceptance window held open before invoice finalizationUnder-billing from late arrivals
A written backfill/correction policy for post-finalization eventsAd hoc handling and inconsistent customer bills
A documented aggregation function that matches the priced metricPromising pricing the meter cannot compute

Where this fits in the ingestion path

Idempotency and late-event handling are properties of the ingestion layer specifically, the boundary where application events become billable records. The rest of the path (queues, rating, aggregation, invoice generation) inherits correctness from that boundary. If you want the broader picture of how a metering ingestion pipeline is structured around validation, dedup, and exactly-once processing, see the companion piece on building a usage ingestion pipeline that does not lose revenue. The patterns here are the parts of that pipeline that decide whether the resulting invoice is correct.

None of this is exotic. It is the boring, load-bearing plumbing that separates a meter you can put on an invoice from an event log you cannot. Get the identifier and the acceptance window right and most billing disputes never happen. Get them wrong and you find out one angry customer at a time.

For a concrete implementation of exactly this, usageDb (the open-source Rust engine behind UsageBox) classifies every ingested event as accepted, duplicate, conflict, or rejected, and rebuilds its dedupe state across restarts from the write-ahead log and raw segments. The mechanics are walked through in Idempotent Metering in usageDb, part of the engine internals series.

Key Topics

  • usage metering
  • idempotency
  • deduplication
  • metered billing
  • Stripe meter events
  • late events
  • ingestion
  • usage-based pricing

Related Articles

Explore more articles on similar topics to deepen your understanding of usage-based billing.

Idempotent Metering in usageDb: Dedupe, Conflicts, and At-Least-Once Collectors

How usageDb guarantees each billable event is counted exactly once: stable event_ids, blake3 128-bit payload hashing, th...

9 min readRead more

Who Spent the Tokens? Cost Attribution Across Tools, Sub-Agents, and Retries (2026)

A single agent run fans out into tool calls, sub-agents, parallel branches, and silent retries, then returns one opaque ...

8 min readRead more

Per-Seat Pricing Can't Survive Agentic Users: The SaaS Margin Math That Breaks in One Loop

If you sell software at a flat per-seat price and your product calls an LLM that bills per token, your margin is a bet t...

6 min readRead more

Explore More Articles

Discover our complete collection of usage-based billing guides and implementation patterns.

View all articles