Stripe metered billing for AI apps: what it solves and what it does not
You are sketching the pricing page for your AI app and every plan ends in "per generation" - because your costs arrive per generation. Stripe meters the charging half beautifully. The question is what happens at request time.
Usage-based pricing is gravity for AI apps
You are sketching the pricing page for your AI image app, and every plan you write down ends the same way: per generation, per thousand tokens, per video second. That is not a trend you are following - it is your cost structure leaking into your pricing, because every request your users make arrives with a provider invoice attached. Usage-based pricing is the natural model for AI apps, and Stripe has first-class support for it. Billing Meters work exactly the way you would hope: you define a meter, you send a meter event every time a user consumes something, Stripe aggregates the events per billing period, and the invoice computes itself. The integration is one API call after each AI request.
// After the AI call succeeds, report usage to Stripe
await stripe.billing.meterEvents.create({
event_name: 'image_generations',
payload: {
stripe_customer_id: customer.id,
value: '4', // four images in this batch
},
});What Stripe metered billing genuinely solves
Be clear-eyed about how much this buys you, because it is a lot. Invoice math at period boundaries - which events fall into which invoice, across time zones and clock skew - is handled. Proration when a customer upgrades mid-cycle is handled. Tax across jurisdictions is handled. Failed payments, retries, and dunning emails are handled. Tiered and graduated per-unit pricing - first thousand generations at one price, everything after at another - is a configuration option, not a project. And every charge comes with an auditable record of the usage that produced it, which is what you will reach for when a customer disputes a bill. These are brutally hard problems, they are not your product, and Stripe solves them better than anything you would build in-house. If your model is purely "pay for what you used, billed monthly in arrears," you send meter events and you may genuinely be done.
Gap #1: metering is not enforcement
Stripe aggregates usage in order to charge for it. It has no answer to a different question: should THIS request be allowed to run right now? There is no canUse() in the Stripe SDK, no atomic check-and-increment, no way to make request five hundred and one fail because the plan says five hundred. For pure arrears billing that is by design - if every unit is billable, every request is allowed, and the only job is counting accurately. But most AI apps do not sell pure arrears. They sell capped plans: "100 generations a month" on Pro, 10 a day on Free, because consumers will not sign up for an open-ended bill and you will not survive an open-ended provider invoice. A cap needs a real-time, race-safe check at request time, before the provider spend happens - and once you build the counters table, the period rollover, and the per-user check that a cap requires, Stripe's meter has become a downstream copy of a system you own. Stripe's acquisition of Metronome in 2026 shows where they see usage-based billing going - enterprise-grade aggregation - which is still the charging half, not the request-time enforcement half.
Gap #2: Stripe knows customers, your limits know users
Look at the meter event payload again: it keys on stripe_customer_id. That is the correct key for billing, because the Customer is who gets charged. It is the wrong key for limits, because the entity you cap is your end user - identified by your user id, which Stripe has never heard of. The mismatch shows up immediately at the edges. Free-tier users have no card on file and usually no Customer object at all, so there is nothing to attach a meter event to. Anonymous trial users - the ones you most need to cap - have no Stripe identity by definition. And in B2B apps the Customer is the workspace, so "each seat gets 50 generations a month" has no natural expression in Stripe at all: the meter sees one customer, your plan sees twelve people. You can force it - create Customers eagerly, stuff your user id into metadata, aggregate client-side - but you are bending a billing identity model into an entitlement identity model, and every bend is code you maintain.
Gap #3: invoices aggregate, dashboards and free tiers do not
Meter events are aggregated for invoicing, and the aggregation pipeline is built for that job: it is allowed to lag, because the invoice is not computed until the period closes. That makes it the wrong source for anything live. "How many premium images did user X generate in the last hour, broken down by model?" is a query against an events table with metadata on every row - not against an invoice line that says image_generations: 1,407. The usage bar in your app, the admin view that catches the runaway user before the bill arrives, the per-model margin analysis - all of them want granular, queryable, near-real-time events, and the meter gives you period totals on billing's schedule. The free tier makes the point sharply: when nothing is charged, Stripe has nothing to meter - a free plan produces no invoice, so it produces no usage record. But the free tier is exactly where limits matter most, because it is the one plan where every marginal request is pure cost.
The architecture that actually works
The clean answer is two layers with different jobs, not one layer stretched over both. At request time, a metering and limits layer owns the counters, the plan definitions, the race-safe reserve, and the per-user analytics - it answers "is this allowed?" before any provider spend and records what actually happened. At billing time, Stripe owns the money: either it charges whatever the metering layer says was used, or it bills flat plans whose caps the metering layer enforces. The seam between them is thin and runs in both directions. On the request path: reserve quota, run the AI call, commit on success or release on failure. On the billing path: Stripe fires a webhook when a subscription renews or changes, and your handler tells the metering layer so quotas reset on the real billing date. Neither layer pretends to do the other's job, and neither contains a half-built copy of the other. Notice what this does to the identity problem from earlier: the metering layer keys on your user id everywhere, and the only place a Stripe Customer id appears is inside the webhook handler, mapped once. Free users live entirely in the metering layer and never touch Stripe; paying users exist in both, joined by that single lookup.
// Request time - the metering layer answers, race-safe
const r = await meter.reserve(userId, 'image_generation', 1);
if (!r.allowed) return jsonError(429, 'limit_reached');
const image = await generateImage(prompt);
await meter.commit(r.reservationId);
// Billing time - Stripe webhook keeps the quota cycle honest
if (event.type === 'customer.subscription.updated') {
const sub = event.data.object;
await meter.upsertSubscription({
userId: userIdFromCustomer(sub.customer),
planId: planIdFromPrice(sub.items.data[0].price.id),
cycleStart: new Date(sub.current_period_start * 1000).toISOString(),
});
}When Stripe alone is enough
None of this means you always need the second layer. There is a real class of product where Stripe Billing Meters cover the whole problem, and adding anything else is over-engineering - typically a B2B API or infrastructure product where every customer has a contract, a card, and a tolerance for variable invoices. The test is not what kind of company you are; it is what shape your pricing takes. Run the checklist honestly:
- Your pricing is pure arrears - users pay for exactly what they used, billed at period end, with no hard caps to enforce mid-cycle
- You have no free tier, so there is no unbilled usage to limit
- Every user of your product maps one-to-one to a Stripe Customer with a payment method on file
- Your buyers are B2B and tolerate the occasional surprising invoice instead of a mid-month cutoff
- You do not need to show users a live "X remaining this period" number inside the product
Two layers, two tools
The honest summary: Stripe is the right tool for the charging half, and most AI apps also need an enforcement half that Stripe was never designed to provide - per-end-user caps, free-tier limits, race-safe checks at request time, granular usage you can query and show. Vevee is that second layer: per-end-user counters, plans with limit groups, and an atomic reserve/commit/release flow, built to sit next to Stripe (or Polar, or any biller) rather than replace it - the webhook pattern above is the whole integration, and the free tier is enough to wire it up before your next pricing meeting.
Keep reading
The race condition in "if (usage < limit)" that is costing your AI app money
A user at nine of ten images opens six tabs and clicks generate in all of them. Every tab passes your limit check, every tab gets an image, and you pay for all six. The bug is one read-then-write - and your unit tests will never catch it.
MeteringHow to build a credits system for your AI app (ledger design, rollover, refunds)
A user emails: "I had 40 credits this morning and now I have 12, and I only made one image." If your balance is a column you mutated, you cannot answer that email. Here is the ledger design that can.