Lesson 24 / 32

Metering Usage for Billing

Record per-tenant usage events reliably, deduplicate retries with idempotency keys, and aggregate them into billable quantities.

By · 15 years in web development

Counting what each tenant uses

Usage-based pricing, quotas and cost analysis all need metering: recording how much of each billable unit a tenant consumes, such as API calls, active users, storage, messages or compute minutes. The usual design emits a usage event whenever something billable happens, containing the tenant, the metric, the quantity, a timestamp and a unique idempotency key. Events flow through a queue into a store where they are aggregated per tenant per billing period. Two properties matter more than anything. No double counting: networks retry, so the same event may arrive twice, and deduplicating by idempotency key prevents overbilling. No silent loss: if events are dropped you undercharge and quotas stop working, so producers should write events durably (for example via an outbox table) rather than fire-and-forget. Keep raw events long enough to answer billing disputes, and reconcile totals against independent sources such as database storage reports.

Deduplicating and aggregating events

The duplicate e1 is a retry and is counted once.

from collections import defaultdict

events = [
    {"key": "e1", "tenant": "acme", "metric": "api_calls", "qty": 120},
    {"key": "e2", "tenant": "zen",  "metric": "api_calls", "qty": 15},
    {"key": "e1", "tenant": "acme", "metric": "api_calls", "qty": 120},  # retried duplicate
    {"key": "e3", "tenant": "acme", "metric": "storage_gb", "qty": 4},
]

seen = set()
usage = defaultdict(int)
for e in events:
    if e["key"] in seen:          # idempotency: count each event once
        continue
    seen.add(e["key"])
    usage[(e["tenant"], e["metric"])] += e["qty"]

for (tenant, metric), qty in sorted(usage.items()):
    print(tenant, metric, qty)

Output:

acme api_calls 120
acme storage_gb 4
zen api_calls 15

Bill from facts, not from logs

Application logs are sampled, rotated and reformatted, which makes them unreliable for money. Emit purpose-built usage events with a stable schema, and treat changing that schema as carefully as changing a database table.

Quick check: A usage event for 120 API calls is delivered twice because of a network retry. What prevents the tenant being billed for 240?

  • Deduplicating on the event's idempotency key
  • Rounding usage down to the nearest hundred
  • Billing only once per year
  • Dropping all events that arrive within one second of each other
Answer

Deduplicating on the event's idempotency key — Each logical event has a unique key, so a retried delivery is recognised and ignored. Time-based dropping would lose genuine events.