Skip to main content

Usage-Based Billing with Stripe in Boilerplates 2026

·StarterPick Team
Share:

TL;DR

Usage-based billing is Stripe's metered billing feature. Instead of fixed monthly prices, customers pay for what they use (API calls, seats, storage, etc.). Implementation requires: (1) a metered price in Stripe, (2) tracking usage in your database, and (3) reporting usage to Stripe at billing period end. Total setup: 2-3 days.


When Usage-Based Billing Makes Sense

Flat monthly pricing is simpler to build and predict, but it fails in two scenarios that matter:

When value is directly proportional to usage: If a customer making 100 API calls gets 100x the value of a customer making 1 call, charging them the same flat rate means either overcharging light users (churn) or undercharging heavy users (revenue leakage). Usage-based pricing aligns cost with value.

When customers have wildly different consumption patterns: A single Slack team might have 5 users or 500. A single API customer might make 1,000 requests/month or 1,000,000. Flat pricing forces you to either pick a price that's too high for small customers or too low for large ones.

The decision framework:

  • < 10x variation in usage across customers → flat pricing is fine, simpler to build
  • 10-100x variation in usage → tiered pricing (free tier + paid tiers with different limits)
  • > 100x variation in usage → pure usage-based or usage-based with a minimum commitment

The most common pattern for early-stage SaaS: flat monthly subscription per plan tier, with soft limits enforced in the application (not by Stripe). Upgrade to Stripe Meters when you have enterprise customers whose usage varies enough that flat pricing creates pricing conflicts.


Stripe Concepts

Before code, understand the Stripe model:

  • Metered price: A price with aggregate_usage: sum and billing_scheme: per_unit
  • Usage records: Events you report to Stripe via API (e.g., "user X made 150 API calls")
  • Billing period: Usage is reset to 0 at the start of each period
  • Invoice: Stripe generates an invoice at period end based on reported usage

Step 1: Create a Metered Price in Stripe

// Run once: create metered price in Stripe
const price = await stripe.prices.create({
  currency: 'usd',
  product: 'prod_XXXXX', // Your product ID
  recurring: {
    interval: 'month',
    usage_type: 'metered',
    aggregate_usage: 'sum',
  },
  billing_scheme: 'per_unit',
  unit_amount: 1, // $0.01 per unit
  // Or use tiered pricing:
  // billing_scheme: 'tiered',
  // tiers_mode: 'graduated',
  // tiers: [
  //   { up_to: 1000, unit_amount: 0 },         // First 1000 free
  //   { up_to: 10000, unit_amount: 1 },         // $0.01 each up to 10k
  //   { up_to: 'inf', unit_amount_decimal: '0.5' }, // $0.005 above 10k
  // ],
});

console.log('Metered price ID:', price.id); // Save this as STRIPE_METERED_PRICE_ID

Step 2: Track Usage in Your Database

// prisma/schema.prisma
model UsageRecord {
  id             String   @id @default(cuid())
  userId         String
  subscriptionId String
  metric         String   // 'api_calls', 'storage_gb', 'seats'
  quantity       Int
  recordedAt     DateTime @default(now())

  user User @relation(fields: [userId], references: [id])

  @@index([userId, metric, recordedAt])
}

model Subscription {
  id                    String   @id @default(cuid())
  userId                String   @unique
  stripeSubscriptionId  String   @unique
  stripeSubscriptionItemId String? // Needed for metered billing
  status                String
  currentPeriodEnd      DateTime
  user                  User     @relation(fields: [userId], references: [id])
}

Step 3: Usage Tracking Service

// lib/usage.ts
import { prisma } from './prisma';
import { stripe } from './stripe';

export async function trackUsage(
  userId: string,
  metric: string,
  quantity: number = 1
) {
  const subscription = await prisma.subscription.findUnique({
    where: { userId },
  });

  if (!subscription) return; // No subscription, no tracking

  // Store locally for fast queries
  await prisma.usageRecord.create({
    data: {
      userId,
      subscriptionId: subscription.id,
      metric,
      quantity,
    },
  });
}

export async function getCurrentUsage(
  userId: string,
  metric: string
): Promise<number> {
  const subscription = await prisma.subscription.findUnique({
    where: { userId },
  });

  if (!subscription) return 0;

  const result = await prisma.usageRecord.aggregate({
    where: {
      userId,
      metric,
      recordedAt: {
        gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000), // Current month
      },
    },
    _sum: { quantity: true },
  });

  return result._sum.quantity ?? 0;
}

Step 4: Report Usage to Stripe

Stripe needs usage reported before invoicing. Two approaches:

Option A: Real-time reporting (simple, more API calls)

// Report to Stripe immediately when usage occurs
export async function trackUsageAndReport(
  userId: string,
  metric: string,
  quantity: number = 1
) {
  const subscription = await prisma.subscription.findUnique({
    where: { userId },
  });

  if (!subscription?.stripeSubscriptionItemId) return;

  // Track locally
  await trackUsage(userId, metric, quantity);

  // Report to Stripe immediately
  await stripe.subscriptionItems.createUsageRecord(
    subscription.stripeSubscriptionItemId,
    {
      quantity,
      timestamp: 'now',
      action: 'increment',
    }
  );
}

Option B: Batch reporting via cron (preferred)

// lib/usage-reporter.ts — run daily or hourly
export async function reportPendingUsage() {
  const unreportedUsage = await prisma.usageRecord.findMany({
    where: { reportedToStripe: false },
    include: { user: { include: { subscription: true } } },
  });

  // Group by subscription + metric
  const grouped = groupBy(
    unreportedUsage,
    r => `${r.user.subscription?.stripeSubscriptionItemId}:${r.metric}`
  );

  for (const [key, records] of Object.entries(grouped)) {
    const [itemId] = key.split(':');
    const total = records.reduce((sum, r) => sum + r.quantity, 0);

    try {
      await stripe.subscriptionItems.createUsageRecord(itemId, {
        quantity: total,
        timestamp: 'now',
        action: 'increment',
      });

      await prisma.usageRecord.updateMany({
        where: { id: { in: records.map(r => r.id) } },
        data: { reportedToStripe: true },
      });
    } catch (err) {
      console.error(`Failed to report usage for item ${itemId}:`, err);
    }
  }
}

// Cron job (Vercel cron or external)
// vercel.json
// { "crons": [{ "path": "/api/cron/usage-report", "schedule": "0 * * * *" }] }

Step 5: Usage in API Routes

// app/api/generate/route.ts — track usage on each API call
import { trackUsageAndReport } from '@/lib/usage';
import { getServerSession } from 'next-auth';

export async function POST(req: Request) {
  const session = await getServerSession();
  if (!session) return new Response('Unauthorized', { status: 401 });

  // Check usage limit before processing
  const currentUsage = await getCurrentUsage(session.user.id, 'api_calls');
  const limit = getPlanLimit(session.user.subscription?.plan, 'api_calls');

  if (currentUsage >= limit) {
    return new Response('Usage limit exceeded', { status: 429 });
  }

  // Process the request
  const result = await processRequest(req);

  // Track usage after success
  await trackUsageAndReport(session.user.id, 'api_calls', 1);

  return Response.json(result);
}

Showing Usage to Users

// components/UsageMeter.tsx
export async function UsageMeter({ userId }: { userId: string }) {
  const current = await getCurrentUsage(userId, 'api_calls');
  const limit = 1000; // Pro plan limit
  const pct = Math.min((current / limit) * 100, 100);

  return (
    <div>
      <div className="flex justify-between text-sm mb-1">
        <span>API Calls</span>
        <span>{current.toLocaleString()} / {limit.toLocaleString()}</span>
      </div>
      <div className="h-2 bg-gray-200 rounded-full">
        <div
          className={`h-2 rounded-full ${pct > 90 ? 'bg-red-500' : 'bg-indigo-600'}`}
          style={{ width: `${pct}%` }}
        />
      </div>
      {pct > 90 && (
        <p className="text-red-600 text-xs mt-1">
          You're at {pct.toFixed(0)}% of your limit.{' '}
          <a href="/billing">Upgrade →</a>
        </p>
      )}
    </div>
  );
}

Handling Overages and Billing Surprises

Usage-based billing creates a risk that flat billing doesn't: customers receive unexpectedly large invoices. A developer whose API integration has a bug might inadvertently consume 10x their expected usage in a single day. The resulting invoice creates a support nightmare and potential chargeback.

Defend against billing surprises with:

Spending caps per period: Let customers set a maximum monthly spend. When the cap is reached, stop serving requests and notify them. Stripe's built-in billing alerts trigger at a dollar threshold and send emails, but application-level enforcement (check usage before serving) is more reliable.

Usage alerts at 80% and 95%: Send a proactive email when a customer approaches their limit. Most customers would rather receive an upgrade prompt than hit a hard limit without warning.

Grace period for overage: For the first overage event, extend the limit by 25% and notify rather than hard-stopping. Convert the situation from a support incident into an upgrade conversation.

Idempotent usage reporting: If you're reporting usage to Stripe and your cron job runs twice due to a crash, you could double-report usage. Use the identifier field on Stripe's Meter Events API to deduplicate. Store a unique event ID per usage event in your database.


Pricing Model Design

Before implementing metered billing, design the pricing model carefully. Common patterns:

Free tier + overage: 1,000 requests/month free, then $0.001/request. Lowers signup friction, but pure pay-as-you-go means no predictable MRR.

Flat + overage: $29/month includes 10,000 requests, then $0.002/request above that. Predictable MRR base plus upside from heavy users. Most common pattern for API products.

Volume tiers: Price decreases as usage increases. First 10,000 requests at $0.002, next 90,000 at $0.001, above 100,000 at $0.0005. Rewards large customers without giving away the product.

Committed use discount: Customer commits to minimum usage (e.g., $500/month minimum) in exchange for lower per-unit pricing. Works for enterprise sales, requires more complex Stripe setup.

For early-stage products, start with flat-rate tiers with soft limits. Add metered billing when you have customers whose usage is predictably much higher than your pricing tiers account for.


Time Budget

TaskDuration
Stripe metered price setup1 hour
Usage tracking schema + service1 day
Stripe reporting (cron or real-time)1 day
Usage display UI0.5 day
Overage handling + alerts0.5 day
Total~3 days

Dunning Management

When a customer's payment fails, Stripe retries the charge automatically using Smart Retries (which uses ML to determine the best retry timing). But automatic retries succeed on roughly 60-70% of failed charges. The remaining 30-40% need intervention.

Dunning — the process of managing failed payment recovery — is critical for minimizing involuntary churn. Involuntary churn (customers whose subscriptions lapse due to payment failures, not by choice) accounts for 20-40% of total SaaS churn and is largely preventable.

The standard dunning sequence:

  1. Day 0 (failure): Stripe sends its default "payment failed" email. Replace this with your own email via Stripe's webhook for invoice.payment_failed.
  2. Day 3 (first retry): Send a friendly reminder. "Having trouble with payment — here's a link to update your card."
  3. Day 7 (second retry): More urgent message. Show what they'll lose access to.
  4. Day 14 (final notice): Last chance before cancellation. Consider offering a discount or payment plan.
  5. Day 21 (grace period end): Downgrade to free tier. Don't delete data — most returning customers come back within 30 days.

Stripe's Customer Portal handles card update flows without any custom code. Link users to stripe.billingPortal.sessions.create() from your payment failure emails.


Trial to Paid Conversion Optimization

If you offer a free trial, the conversion from trial to paid is the most important metric in your business. The subscription tracking you build in steps 2-3 enables several conversion optimization tactics:

Usage-based trial alerts: Track whether a trial user has reached the key activation event (created a project, connected an integration, imported data). If they're 10 days into a 14-day trial without activating, send a proactive email with help to get them started. Activated trial users convert at 3-5x the rate of inactive ones.

Limit-based upgrade prompts: Rather than time-based trials, consider usage-based limits that prompt upgrades naturally. When a free user creates their 3rd project (limit is 3), show an upgrade modal in-app: "You've hit the project limit. Upgrade to Pro for unlimited projects." This in-context prompt converts better than an end-of-trial email.

Grandfathered pricing incentive: Offer early users a lower price locked in forever if they upgrade during their trial. "Pro is normally $29/month — upgrade now during your trial and lock in $19/month forever." Creates urgency and rewards early adopters.


LemonSqueezy as an Alternative

Stripe is the dominant payment processor for SaaS, but LemonSqueezy is a growing alternative specifically for indie developers and micro-SaaS. Key differences:

Merchant of Record: LemonSqueezy acts as the merchant of record, handling VAT/GST collection and remittance globally. With Stripe, you're responsible for tax compliance. For solo developers selling globally, LemonSqueezy's built-in tax handling is a significant operational advantage.

Simplicity: LemonSqueezy's dashboard and API are simpler than Stripe's. Fewer configuration options, but faster to get up and running for simple subscription use cases.

Higher fees: LemonSqueezy charges 5% + $0.50 per transaction vs Stripe's 2.9% + $0.30. For early-stage products with low revenue, the tax compliance simplification is worth the higher fee. At scale, Stripe's lower fees and flexibility win.

ShipFast ships with both Stripe and LemonSqueezy integrations, letting you choose at setup time.


For API products where every request is billable, best boilerplates for building API products covers the full Hono + Unkey + Stripe Meters stack with per-key rate limiting. For the admin panel where you monitor subscription and usage data, how to add an admin dashboard to your boilerplate covers the usage monitoring tables. For AI features where token costs drive your pricing, how to add AI features to any SaaS boilerplate covers per-user token tracking with the Vercel AI SDK.


Methodology

Implementation patterns based on Stripe Meters API documentation (2026) and Stripe's official usage-based billing guide. Pricing model patterns sourced from community discussion in the Indie Hackers forum and ShipFast Discord.

Stripe's webhook handling is the most common integration failure point. Always verify webhook signatures, implement idempotency keys for payment processing, and test the full failure path — including what happens when a customer's card declines on renewal — before launching.

Compare boilerplates with usage-based billing support on StarterPick.

Check out this boilerplate

View ShipFaston StarterPick →

The SaaS Boilerplate Matrix (Free PDF)

20+ SaaS starters compared: pricing, tech stack, auth, payments, and what you actually ship with. Updated monthly. Used by 150+ founders.

Join 150+ SaaS founders. Unsubscribe in one click.