How to Process Usage Events Idempotently
Consider a retry after a timeout, a webhook delivered twice, or two workers that consume the same message in parallel. Idempotent processing of usage events protects invoice amounts, revenue recognition, the tax base and the month-end close from the silent discrepancies these cases cause, so the decision reaches well beyond an API detail. A double-counted API call starts as a data error. It can then generate a wrong invoice, trigger a SEPA direct debit and later force finance and accounting into a correction process.
For usage-based and hybrid SaaS models, idempotency is therefore a business control principle. The requirement is that the same economic event enters the billable quantity exactly once, regardless of delivery attempts, delivery order and parallel processing.
What idempotent processing means for usage events
Idempotency is often reduced to an HTTP header. A client sends an Idempotency-Key, the API recognizes the repeated request and returns the same response. That is useful, but for metering it is not enough on its own. What matters is that the billing platform does not rate the same usage again, aggregate it again or carry it into a billing document that has already been posted.
A usage event therefore needs a stable business identity. That identity must not be derived from the time the API received the request, because a request can arrive late or be sent again. It should come from the producing system instead, for example from a unique event ID issued by the product service, a transaction ID, or a deterministic combination of source system, object and business operation.
A reliable event contains at least a tenant, the customer or account reference, a metric, a usage timestamp, the quantity and an event ID. For an API, it could look like this:
{
"event_id": "evt_01JQ8P9X7A3K",
"account_id": "acc_4821",
"metric": "api_requests",
"quantity": 250,
"occurred_at": "2026-08-24T10:14:03Z",
"source": "api-gateway"
}
If exactly this event arrives again, the billable state must not change. The event ID is an invariant: the same ID must always carry the same business meaning. If an identical ID comes in with a different quantity or a different account, the system must not treat it as a retry, because it is an integrity conflict. The API should reject it visibly instead of silently preferring one of the values.
Processing usage events idempotently: the technical core
The most effective safeguard combines a unique database constraint on the business deduplication key with atomic processing. A simple pre-check along the lines of "does this ID already exist?" is not enough. Between the read and the write, a second worker can process the same event. Under load, this turns into a classic race condition that books the quantity twice.
The correct flow is transactional. The system first persists the raw event with a unique key. Only if that insert succeeds does the usage go into the metering aggregate. If the insert fails because of a key conflict, the system makes no further change to any aggregate. The response can still report success, because the desired state has already been reached.
In distributed architectures, you also have to be clear about what exactly is atomic. If event ingestion, aggregation, pricing and invoice generation run in separate services, no single database transaction can cover all the steps. Each handoff then needs its own idempotency boundary. An outbox pattern makes sure that an accepted event is reliably published for further processing. Consumers in turn store their own processing status under a unique constraint.
Over a network, "exactly once" is rarely a realistic delivery guarantee. Queues and webhooks typically deliver at least once. So the goal is to deliver at least once and apply the business effect once. Keeping these two apart prevents a dangerous false precision in architecture decisions.
Match the key to the business granularity
A global event key is simple, but it is not always correct. When several producers generate IDs in separate namespaces, the key has to be something like (tenant_id, source, event_id). Otherwise an event from another tenant can be blocked by mistake. In a marketplace, the seller or the sub-account can also be part of the business boundary.
There is a trade-off. The broader the key, the higher the risk of collisions. The narrower it is, the easier it becomes for the same economic transaction to come in more than once. Do not derive the key from incidental technical values such as a container ID or a request UUID from the API gateway. It has to identify a transaction that stays the same through retries, replays and incident recovery.
Corrections are not duplicates
A particularly common mistake is overwriting events that have already been accepted. Say a product service first reports 250 units and later finds that 20 of them were invalid. A second event with the same ID and a quantity of 230 is not idempotent. It retroactively changes the meaning of an immutable event and destroys traceability.
Corrections need their own business events, for example a negative adjustment that references the original event. The system can then explain both the current billable quantity and the full history. Finance depends on this difference: a ledger has to show why a base amount changed, and a current value stored without its origin cannot show that.
Whether a correction may still affect the same invoice depends on the status of the billing period. Before invoicing, it can update the draft. After finalization, it has to result in a cancellation, a credit note or a follow-up invoice, depending on the tax jurisdiction, the invoice status and the process. If you simply write late events back into the previous month, you risk differences between the invoice, revenue deferrals and the DATEV export.
Late arrivals need explicit rules
The time of usage and the time of receipt are different data points. Pricing and the billing period usually follow occurred_at, while operational processing and audit trails also need received_at. An event from August 31 that arrives on September 2 is not automatically September usage.
Define a late-arrival window for each metric and product model. For API requests, a few hours may be enough. For data imports or offline workloads, several days are plausible. Once the window has closed, the system should create a documented adjustment instead of silently writing into a closed period. That is less convenient than changing data after the fact, but it fits invoice and ledger processes that have to stand up to an audit.
Idempotency does not end at metering
Blocking duplicate usage events does not yet protect the whole revenue chain. The downstream processes need the same discipline. Invoice creation may produce only one final document per billing period and account. Payment collection needs a unique payment instruction. Revenue recognition has to detect a journal line that already exists when data is reprocessed, instead of duplicating IFRS 15 or ASC 606 schedules.
The same applies to compliance data. Once an invoice under EN 16931 or an XRechnung has been finalized, a new usage run must not give it a new amount. For EU VAT OSS, reverse charge and tax reports, corrections have to enter the tax logic as traceable follow-up documents. Idempotency is what links product telemetry to financial figures you can rely on.
Kontier treats this chain as billing infrastructure instead of a loose series of webhooks. The product catalog, metering, invoicing and payments, the ledger and the export work with traceable status transitions. That reduces duplicates in the event store and also cuts manual reconciliation between engineering and finance.
What teams should measure
Unit tests alone do not show whether idempotency works. In production you need metrics for repeated events, conflicts with a differing payload, late events, and events rejected because they fall outside the allowed time window. A rising duplicate rate can point to aggressive client retries, queue disruptions or faulty webhook providers. A rising conflict rate more likely indicates a bug in the producer or unclear event semantics.
Auditability also requires that deduplicated requests do not just disappear. Store when an event was first accepted, which payload checksum it had and when identical repeats came in. The raw data does not have to stay online forever, but retention and archiving have to fit your contracts, your tax obligations and your GoBD strategy. A retention period that is too short saves storage and can make it impossible to restore old billing periods.
Test the system deliberately against real conditions: send the same message ten times in parallel, kill the consumer after it persists the event but before the ack, deliver events out of order, and load a correction after the invoice has been finalized. If quantities, invoice statuses and ledger lines can still be explained afterward, the architecture is ready for growth. Retries then become part of normal operations and stop triggering the next Excel reconciliation in the month-end close.