Stripe Metered Billing Configuration

Usage-based pricing is only as trustworthy as the meter behind it, and on Stripe that meter is now the Billing Meters API — meters and meter events, not the deprecated subscription usage records you may have seen in older tutorials. Part of the Designing API Pricing Tiers guide, this page walks through creating a meter, choosing the right aggregation, reporting usage from a FastAPI app, reconciling reported events against the invoice, and hardening the whole loop against the failure modes that quietly leak revenue. It pairs with integrating Stripe with Python APIs and how to charge for API access using Stripe — read those first if you have not yet stood up the basic Stripe integration and checkout.

The commercial stakes here are direct. A metered bill is a number you show a customer once a month with your name attached to it. If that number is wrong high, you get disputes and churn; if it is wrong low, you eat the difference on every invoice for the life of the account. Getting the meter right is not a nice-to-have — it is the difference between a margin you can forecast and one you discover after the fact.

How Stripe metered billing fits together

The current model has four moving parts. A Meter defines what you count and how — an event_name like api_requests and an aggregation like sum. A meter event is a single reported unit of usage, tied to a customer. A Price is configured as metered and bound to the meter. A Subscription to that price produces an invoice at period end, where Stripe rolls up the meter events into a billable quantity. Your job is to report meter events accurately as your API does work, then verify the rolled-up total against your own counter before trusting the invoice.

The flow is one-directional: usage event → meter aggregation → invoice line. You never write to the invoice; you write events, and Stripe aggregates. That separation is what makes the system idempotent-friendly, and it maps directly onto the usage-based pricing model you chose your billing axis around. It also means your own usage event log in Postgres is not redundant with Stripe — it is the ground truth you reconcile the meter against.

Stripe metered billing flow A usage event from the API is reported as a meter event, Stripe aggregates events into a meter total, and the subscription produces an invoice line. API request does work Meter event v1/billing Meter sum/period Invoice line

Choosing an aggregation before you write code

The aggregation you pick on the meter is not cosmetic — it changes what a meter event means, and you cannot swap it later without creating a new meter and migrating subscriptions. Stripe gives you three practical formulas. sum adds the value of every event, which is what you want when each request can consume a variable amount (tokens processed, megabytes rendered, rows exported). count ignores value and tallies events, which fits a flat per-call charge. last takes the most recent value in the window, which suits gauge-style metrics like "seats currently active" or "GB currently stored".

Pick the one that matches your billing axis exactly. If you charge per API call, use count and stop sending a value you do not need. If you charge per token — the common shape for anything wrapping an LLM, which the guide on controlling LLM API costs in production digs into — use sum and report the token count as value. Getting this wrong is expensive: a meter set to count when you meant sum bills every 4,000-token request as a single unit, and you will not notice until margin analysis shows revenue that cannot cover compute.

The metered Price you bind to the meter is a separate decision. A flat per-unit price is simplest, but Stripe also supports graduated and volume tiers — the first 100k units at one rate, the next 900k cheaper. Model that curve against your real cost per API request so every tier stays above your marginal cost, not just the first one.

Step 1: Create the meter

You create the meter once, in the dashboard or via the API. The event_name is the contract between your code and Stripe — every meter event you send must carry it. The customer_mapping tells Stripe which payload field identifies the customer, and value_settings names the payload field Stripe reads for sum aggregation.

Python
import os
import stripe

stripe.api_key = os.getenv("STRIPE_SECRET_KEY")

meter = stripe.billing.Meter.create(
    display_name="API Requests",
    event_name=os.getenv("METER_EVENT_NAME", "api_requests"),
    default_aggregation={"formula": "sum"},
    customer_mapping={"event_payload_key": "stripe_customer_id", "type": "by_id"},
    value_settings={"event_payload_key": "value"},
)
print(meter.id)  # store this; bind a metered Price to it in the dashboard

Then create a metered Price bound to this meter (dashboard or API), and subscribe customers to it. Store the resulting stripe_customer_id on each of your accounts so you can address meter events to the right customer. Do this in a test-mode workspace first and drive a full billing period with Stripe test clocks before a single real event flows — a meter you misconfigured is far cheaper to fix before it has issued an invoice.

Step 2: Report usage from FastAPI

Report a meter event after the work succeeds, and do it off the request's critical path. A background task or a queue keeps billing from adding a Stripe round trip to every response. Pass an identifier for idempotency so a retried report is counted once.

Python
import os
import stripe
from fastapi import FastAPI, BackgroundTasks, Depends, Header

app = FastAPI()
stripe.api_key = os.getenv("STRIPE_SECRET_KEY")
EVENT_NAME = os.getenv("METER_EVENT_NAME", "api_requests")

def report_meter_event(stripe_customer_id: str, units: int, identifier: str) -> None:
    """Report usage to Stripe. identifier makes the report idempotent on retries."""
    try:
        stripe.billing.MeterEvent.create(
            event_name=EVENT_NAME,
            identifier=identifier,  # dedupes replays within Stripe's window
            payload={"stripe_customer_id": stripe_customer_id, "value": str(units)},
        )
    except stripe.StripeError as exc:
        # Never crash the request path on a billing hiccup; log and let reconciliation catch it.
        print(f"meter report failed for {stripe_customer_id}: {exc}")

async def current_customer(x_api_key: str = Header(...)) -> str:
    # Replace with a real lookup mapping the API key to its Stripe customer id.
    return os.getenv("DEMO_STRIPE_CUSTOMER_ID", "cus_demo")

@app.post("/v1/process")
async def process(bg: BackgroundTasks, request_id: str, customer: str = Depends(current_customer)):
    result = {"processed": True}            # ... do the real work ...
    bg.add_task(report_meter_event, customer, 1, identifier=request_id)
    return result

The identifier is the linchpin. Use a value that is unique per billable unit of work — your own request id, not a timestamp — so Stripe deduplicates replays and your bill stays correct under retries. Keep a local copy of every reported (customer, units, identifier) so you can reconcile later. The diagram below shows why the ordering matters: the customer's response returns as soon as the work is done, and the meter report happens after, on a lane that can fail without the caller ever noticing.

Billing report kept off the request critical path The client receives its response as soon as the API finishes work, then a background task reports the meter event to Stripe on a separate lane. Client Your FastAPI app Stripe meter POST /v1/process do work 200 OK (fast path) MeterEvent.create background task, may retry

For anything above a few hundred requests per second, a per-request BackgroundTasks call is the wrong tool — you want a queue. Push (customer, units, identifier) onto Redis or a Celery-style background job and drain it with a worker that batches events. That also survives a process restart, which an in-process background task does not.

Step 3: Reconcile against the invoice

Trust, then verify. Before a period's invoice finalizes, read Stripe's aggregated total for the customer and compare it to your local counter. A drift means dropped events (report failures) or duplicates (a broken identifier) — both are revenue bugs, and both are invisible until you look for them.

Python
import os
import stripe

stripe.api_key = os.getenv("STRIPE_SECRET_KEY")

def stripe_meter_total(meter_id: str, customer_id: str, start: int, end: int) -> int:
    """Pull Stripe's aggregated usage for a customer over a window."""
    summaries = stripe.billing.Meter.list_event_summaries(
        meter_id,
        customer=customer_id,
        start_time=start,
        end_time=end,
    )
    return int(sum(float(s["aggregated_value"]) for s in summaries.auto_paging_iter()))

def reconcile(meter_id: str, customer_id: str, start: int, end: int, local_count: int) -> dict:
    reported = stripe_meter_total(meter_id, customer_id, start, end)
    drift = reported - local_count
    return {"stripe": reported, "local": local_count, "drift": drift, "ok": drift == 0}

Run reconciliation on a schedule near period close, well before Stripe finalizes the invoice. The decision below is the whole discipline: compare the two totals, and if they disagree, fix the events while you still can. Once an invoice is paid, correcting it means a manual credit or an out-of-band charge, both of which cost you support time and customer goodwill.

Reconciliation decision before invoice finalizes Compare Stripe's aggregated total against the local counter; zero drift finalizes the invoice, non-zero drift branches to investigate dropped or duplicated events. Local counter 1,000,000 Stripe aggregate 999,880 drift == 0 ? yes Let invoice finalize no (-120) Investigate before finalize dropped reports or dup identifiers

Failure modes that quietly cost you money

Metered billing fails silently, which is what makes it dangerous. Four modes account for almost every incident. Dropped events: your MeterEvent.create raised, you logged it, and nobody topped up the report — the customer is under-billed and you never sent the revenue. Duplicate identifiers: two code paths generated the same request id or you fell back to a timestamp under load, so Stripe deduped legitimate distinct events and under-counted, or a non-unique identifier let a replay through and over-counted. Wrong customer mapping: an API key mapped to the wrong stripe_customer_id, so one account's usage lands on another's invoice — the kind of bug that turns into a refund and an apology. Clock and window edges: an event reported at 23:59:59 on the last day of the period can land in the next window depending on Stripe's timestamping, shifting revenue between invoices.

The defense against all four is the same pair of habits you have already built: a local event log that is the source of truth, and a reconciliation job that runs before every finalize. Treat the reconciliation drift as a monitored metric, not a script you run by hand — alert on any non-zero value the way you would alert on a 500. If your free tier feeds into the same meter, the abuse patterns in preventing free-tier abuse are the same events distorting your paid totals, so the two problems share a fix.

Cost and performance at scale

Reporting meter events to Stripe is free — there is no per-event fee — so the cost of metered billing is not on your Stripe bill, it is in latency and compute. That is exactly why Step 2 keeps the report off the request path. Reporting synchronously means every billable request waits on a Stripe API round trip, which in practice adds 100–150 ms to your p95 for zero customer benefit. Moving the report to a background task or queue erases that, and batching in the worker keeps API-call volume flat as traffic grows.

p95 request latency by reporting strategy Synchronous reporting pushes p95 latency to about 185 ms; a background task drops it to about 58 ms; a batched queue worker to about 52 ms. 200 100 0 p95 ms Synchronous 185 ms Background 58 ms Batched queue 52 ms

At five million billable requests a month, the compute to report every event through a batching worker is a rounding error — a fraction of a vCPU-hour — as long as you are not paying for it in tail latency. The real scaling concern is reconciliation read volume: list_event_summaries paginates, so a job that reconciles thousands of customers near period close should run incrementally through the period, not in one burst at midnight. Feed those same aggregates into a customer usage dashboard so customers can watch their own meter climb — an in-product usage view is the cheapest support-ticket deflector metered billing has.

Configuration reference

Env varDefaultProduction note
STRIPE_SECRET_KEYnoneServer-side only; never log it
METER_EVENT_NAMEapi_requestsMust match the meter's event_name exactly
DEMO_STRIPE_CUSTOMER_IDcus_demoReplace with a real per-account lookup

Builder verdict

Use the Billing Meters API, not the old usage-record endpoints — Stripe deprecated those, and tutorials that still show SubscriptionItem.create_usage_record will lead you down a dead end. Get four things right and metered billing is boring in the good way: choose the aggregation that matches your billing axis before you write code, report events off the request path, make every report idempotent with a stable identifier, and reconcile Stripe's aggregate against your own counter before each invoice finalizes. Skip reconciliation and you will not discover dropped or duplicated events until a customer disputes a bill — which is the most expensive possible time to learn your meter was wrong.

FAQ

Does Stripe still use usage records for metered billing? No. Stripe replaced the older SubscriptionItem usage-record flow with the Billing Meters API — meters and meter events. New integrations should create a meter, bind a metered price to it, and report MeterEvent objects. Existing usage-record integrations should plan a migration, because the deprecated path will not receive new features and eventually stops accepting reports.

How much does metered billing cost to run at five million requests a month? Almost nothing on Stripe's side — reporting meter events carries no per-event fee. Your cost is compute and latency, and it stays low as long as you report off the request path and batch events in a worker: a fraction of a vCPU-hour for the reporting, plus modest read volume during reconciliation. Report synchronously and you instead pay 100–150 ms of p95 latency on every billable request.

How do I avoid double-billing on retries? Pass a stable, unique identifier on every meter event — your own request id, never a timestamp. Stripe deduplicates events that share an identifier within its window, so a replayed report is counted once. Keep a local copy of reported events so reconciliation can catch any identifier that leaked through non-unique.

Should I report usage synchronously in the request? No. Report meter events in a background task or queue after the work succeeds, so a slow or failing billing call never adds latency to the API response. A reporting failure should log and fall through to reconciliation, not break the request. Above a few hundred requests per second, move from BackgroundTasks to a durable queue so reports survive a restart.

How do I catch metering errors before they hit a customer's invoice? Reconcile near period close: pull Stripe's aggregated meter total for each customer with list_event_summaries and compare it to your local counter. Any drift signals dropped or duplicated events; fix it before the invoice finalizes, since post-payment corrections require manual credits or charges. Treat drift as a monitored metric and alert on any non-zero value.

Same track:

Other tracks: