Skip to main content
INS // Insights

Building a Usage Metering and Billing System on AWS

Updated August 2026 · 5 min read

Usage-based pricing is common in modern SaaS, but the engineering behind it gets underestimated at the planning stage. Teams often reach for a billing platform (Stripe Billing, Metronome, Orb) assuming the hard part is invoice generation and payment processing — which those tools genuinely solve well — while underestimating that the harder, more foundational problem is accurate, auditable usage metering upstream of any billing engine. If your metering data is wrong, no billing platform downstream fixes that.

Why Metering Is the Hard Part, Not Billing

A billing platform needs a reliable usage record as input: "customer X consumed Y units of metric Z during period P." Producing that record accurately, exactly once (not double-counted, not dropped), at the volume a production system generates events, is a distributed systems problem independent of anything the billing platform itself handles. Get this wrong and you either overcharge customers (a trust and potentially legal problem) or undercharge (a revenue leak that compounds over time).

The Metering Pipeline Architecture

# Event-driven usage metering with idempotent aggregation
import boto3

def record_usage_event(event, context):
    for record in event["Records"]:
        payload = json.loads(record["body"])
        idempotency_key = payload["event_id"]  # client-generated, unique per real event

        dynamodb.put_item(
            TableName="usage_events",
            Item={
                "idempotency_key": {"S": idempotency_key},
                "customer_id": {"S": payload["customer_id"]},
                "metric": {"S": payload["metric"]},
                "quantity": {"N": str(payload["quantity"])},
                "timestamp": {"S": payload["timestamp"]},
            },
            ConditionExpression="attribute_not_exists(idempotency_key)",
        )

The architecture has distinct stages: event ingestion (via SQS or Kinesis, depending on volume and ordering needs), idempotent storage keyed on a client-generated event ID to prevent double-counting from retries, periodic aggregation (a scheduled job rolling raw events into per-customer, per-period totals), and a clean export interface that feeds whatever billing platform sits downstream.

Idempotency Is Not Optional

Distributed event-driven systems retry — a Lambda function times out after successfully writing to the database but before acknowledging the message, a network blip causes a client to resend an event. Without an idempotency key checked at write time, these retries become double-billed usage. This is the single most common root cause we see in metering bugs that surface as customer billing disputes, and it needs to be designed in from the start rather than patched in after a dispute reveals the gap.

Handling Late-Arriving and Out-of-Order Events

Usage events don't always arrive in strict chronological order, especially from distributed client sources with variable network conditions. The aggregation job needs a defined cutoff policy — how long after a billing period closes will late-arriving events still be counted — and that policy needs to be documented and consistent, since an undefined or inconsistently-applied cutoff is itself a source of billing disputes.

Building the Audit Trail Customers (and Finance) Can Trust

For usage-based billing specifically, customers scrutinize their bill more than they would a flat subscription charge, and disputes are common enough that the system needs a clear answer to "show me exactly which events contributed to this line item." This means retaining raw usage events (not just aggregated totals) for a reasonable window, with an internal tool or API that can reconstruct any invoice line item back to its underlying events on demand.

# Reconstructing a billing line item from raw events for dispute resolution
def get_events_for_invoice_line(customer_id, metric, period_start, period_end):
    return dynamodb.query(
        TableName="usage_events",
        KeyConditionExpression="customer_id = :cid AND timestamp BETWEEN :start AND :end",
        FilterExpression="metric = :metric",
        ExpressionAttributeValues={
            ":cid": {"S": customer_id}, ":metric": {"S": metric},
            ":start": {"S": period_start}, ":end": {"S": period_end},
        },
    )

Where a Billing Platform Takes Over

Once metering produces a reliable, auditable per-customer usage total for a period, handing that off to Stripe Billing, Metronome, or a similar platform for invoice generation, proration, tax handling, and payment collection is the right call — those platforms solve genuinely hard problems in that layer well, and rebuilding them yourself is rarely worth it. The build effort belongs in the metering layer specifically, where accuracy and auditability determine whether the billing platform's output is trustworthy at all.

This build pattern is part of our full-stack development capability, alongside the related decision framework in buy vs. build SaaS decision.

Discuss your project: 907-841-8407 or contact@rutagon.com.

Discuss your project →

Frequently Asked Questions

Should we build metering ourselves or use a platform that includes both metering and billing?

Some platforms (Metronome, in particular) offer metering ingestion alongside billing, which can be the right choice if their event model fits your usage patterns — the build-vs-buy decision should come down to whether their ingestion model handles your specific volume, ordering, and idempotency needs, not just their billing feature set.

How do you prevent double-counting usage events during a system retry?

By requiring a client-generated idempotency key on every event and enforcing a conditional write (reject if the key already exists) at the storage layer — this makes retries safe rather than requiring perfect exactly-once delivery from the transport layer.

What happens if a usage event arrives after we've already generated the invoice for that period?

This requires a documented late-event policy — some systems apply a grace period before finalizing a period's aggregation, others correct the following period's invoice with an adjustment line item; the choice should be explicit and disclosed to customers, not accidental.

How long should raw usage events be retained?

Long enough to support dispute resolution and any regulatory or contractual retention requirement — many systems retain raw events for a year or more even after aggregating them into monthly totals, specifically to support "show me the underlying events" requests.

Does this metering architecture work for both API-call-based metering and continuous resource-consumption metering?

The same event-driven, idempotent-ingestion pattern applies to both, though continuous metrics (compute-hours, storage-GB-hours) typically need a periodic snapshot/sampling approach layered on top rather than a pure discrete-event model.