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.
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.
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.
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.
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.
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.
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.
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 var | Default | Production note |
|---|---|---|
STRIPE_SECRET_KEY | none | Server-side only; never log it |
METER_EVENT_NAME | api_requests | Must match the meter's event_name exactly |
DEMO_STRIPE_CUSTOMER_ID | cus_demo | Replace 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.
Related
Same track:
- Designing API Pricing Tiers — the guide this page sits under, framing where metered billing fits your tiers.
- How to Charge for API Access Using Stripe — the checkout and subscription setup you need before metering.
- Usage-Based vs Seat-Based Pricing — decide the billing axis your meter counts along.
- Calculating Cost per API Request — set meter prices above your true marginal cost.
Other tracks:
- Logging API Usage Events to Postgres — the local source of truth you reconcile the meter against.
- Testing Stripe Integrations with Test Clocks — drive a full billing period before real events flow.
- Verifying Stripe Webhook Signatures — secure the invoice and payment webhooks that close the loop.