Usage-Based Billing with Stripe in Next.js: Meters, Quotas, and Invoices
Stripe
Billing
Usage-Based Pricing
Next.js
SaaS

Usage-Based Billing with Stripe in Next.js: Meters, Quotas, and Invoices

How to implement metered billing that doesn't leak money: Stripe's Meters API, local-first quota enforcement, base-plus-overage pricing, and the four failure modes to design against.

July 10, 20264 min read

Usage-based billing is where SaaS pricing is heading — every AI product bills by tokens, requests, or seats-plus-usage. It's also where billing bugs get expensive: overcharge and you refund with apologies; undercharge and the money is simply gone. Here's how we implement metered billing with Stripe in Next.js, based on production systems we've shipped at PolarSoftBD.

The three usage-based models (pick one deliberately)

  1. Pure metered — pay per unit consumed ($0.01 per API call). Simple to reason about, revenue is volatile.
  2. Base + overage — subscription includes N units, meter beyond that ($29/mo includes 10k calls, then $0.005/call). The most common model, and usually the right one.
  3. Prepaid credits — customers buy credit packs that draw down. Best cash flow, best for AI products. (We cover this in depth in Building AI Credit Systems.)

Stripe's modern metering: the Meters API

Stripe replaced legacy usage records with Billing Meters. You define a meter once, then stream events at it:

// Report usage — call this from your API middleware/job
await stripe.billing.meterEvents.create({
  event_name: "api_requests",
  payload: {
    stripe_customer_id: customer.stripeId,
    value: "1",
  },
  // identifier makes retries safe — Stripe dedupes on it
  identifier: `req_${requestId}`,
});

Then a price attached to that meter does the aggregation and invoicing automatically. The identifier field is your idempotency key — always set it, because your job queue will retry.

The architecture that survives production

Do not call Stripe synchronously in your request path. The pattern that works:

API request → increment local counter (Redis/Postgres)
                    ↓ (batched, every 1–5 min)
              aggregation job → stripe.billing.meterEvents
                    ↓
              Stripe invoices at period end

Local-first metering gives you three things Stripe alone can't:

  • Real-time quota enforcement — you can't ask Stripe "how much has this customer used?" on every request; you need the number in Redis
  • An audit trail — when a customer disputes an invoice, you show them per-request logs
  • Resilience — if Stripe is briefly down, usage queues instead of vanishing
// Quota check in middleware — reads local state, never Stripe
const used = await redis.incrby(`usage:${customerId}:${period}`, cost);
if (used > plan.includedUnits + plan.hardLimit) {
  return new Response("Quota exceeded", { status: 429 });
}

Base + overage in practice

Model it as a subscription with two items: a flat price and a metered price with included units handled via graduated tiers:

await stripe.subscriptions.create({
  customer: customerId,
  items: [
    { price: "price_base_29" },          // flat $29/mo
    { price: "price_metered_overage" },  // tiered: first 10k @ $0, then $0.005
  ],
});

Configure the metered price with graduated tiers: tier 1 up_to: 10000 at $0, tier 2 up_to: inf at $0.005. The "free included units" live in Stripe's tier config, not your application code — one less thing to get wrong.

The four failure modes to design against

  1. Double reporting — job retries send the same usage twice. Fix: deterministic identifier per usage batch.
  2. Clock skew at period boundaries — usage from 23:59 lands in next month's invoice. Decide policy (bill in arrears is fine), document it, be consistent.
  3. Unbounded exposure — a customer's runaway script generates a $40,000 invoice overnight. Fix: hard limits in middleware + billing alerts via billing.alert.triggered webhooks.
  4. Refund asymmetry — refunds and plan downgrades mid-period. Proration on metered items doesn't work like flat items; test this path explicitly before launch.

What customers see (this decides churn)

Usage-based billing dies without transparency. Ship these with v1, not "later":

  • A live usage dashboard (read from your local counters, not Stripe)
  • Email alerts at 75% / 90% / 100% of included usage
  • Upcoming-invoice preview via stripe.invoices.createPreview

Surprise invoices are the #1 cause of usage-billing churn and disputes. An informed customer at 90% upgrades; a surprised one at 130% files a chargeback.

Ship it without the six-week debugging arc

Metered billing touches your API layer, your data model, your background jobs, and your customer dashboard — it's a system, not a feature. We build it as one:

Related: Building AI Credit Systems for SaaS · Complete Lemon Squeezy Webhook Guide

Last updated July 13, 2026

Get thoughtful updates

Join our monthly digest for founders and builders.

Work with PolarSoftBD

Need help shipping your next product? Let's talk.

Start a project →