Thursday, January 8th 2026 ยท 2 min read

How to Implement Metered Billing with Stripe in Next.js

Learn how to implement usage-based metered billing with Stripe in your Next.js SaaS. Covers metered subscriptions, usage reporting, and real-time tracking.

Metered billing (also called usage-based billing) charges customers based on how much they use your service rather than a flat subscription fee. It's perfect for APIs, AI services, cloud storage, or any product where usage varies significantly between customers.

In this guide, we'll implement metered billing with Stripe in a Next.js application.

What is Metered Billing?

Unlike traditional subscriptions where customers pay a fixed monthly fee, metered billing charges based on actual usage:

Billing ModelExampleBest For
Flat Rate$29/monthPredictable services
Tiered$29 for 1000 units, $49 for 5000Growing usage
Metered$0.01 per API callVariable usage
Hybrid$29/month + $0.001 per callBase + overages

Setting Up Stripe for Metered Billing

Step 1: Create a Metered Price in Stripe

First, create a product with a metered price in Stripe Dashboard or via API:

// scripts/create-metered-price.ts
import Stripe from 'stripe';

const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);

async function createMeteredProduct() {
  // Create the product
  const product = await stripe.products.create({
    name: 'API Usage',
    description: 'Pay-per-use API access'
  });

  // Create a metered price
  const price = await stripe.prices.create({
    product: product.id,
    currency: 'usd',
    recurring: {
      interval: 'month',
      usage_type: 'metered', // Key setting!
      aggregate_usage: 'sum' // Sum all usage in billing period
    },
    billing_scheme: 'per_unit',
    unit_amount: 1 // $0.01 per unit (in cents)
    // 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: 'inf', unit_amount: 0.5 } // $0.005 each after
    // ]
  });

  console.log('Product ID:', product.id);
  console.log('Price ID:', price.id);
}

createMeteredProduct();

Step 2: Subscribe Customer to Metered Plan

When a customer subscribes, create a subscription with the metered price:

// actions/billing.ts
'use server';

import { auth } from '@/auth';

import { db } from '@/lib/db';
import { stripe } from '@/lib/stripe';

export async function createMeteredSubscription() {
  const session = await auth();
  if (!session?.user?.id) {
    throw new Error('Unauthorized');
  }

  const user = await db.user.findUnique({
    where: { id: session.user.id },
    select: { stripeCustomerId: true }
  });

  if (!user?.stripeCustomerId) {
    throw new Error('No Stripe customer');
  }

  // Create subscription with metered price
  const subscription = await stripe.subscriptions.create({
    customer: user.stripeCustomerId,
    items: [
      {
        price: process.env.STRIPE_METERED_PRICE_ID!
        // No quantity needed for metered prices
      }
    ]
    // Optionally add a base fee
    // add_invoice_items: [{
    //   price: 'price_base_monthly_fee'
    // }]
  });

  // Store subscription info
  await db.user.update({
    where: { id: session.user.id },
    data: {
      stripeSubscriptionId: subscription.id,
      // Store the subscription item ID - needed for usage reporting
      stripeSubscriptionItemId: subscription.items.data[0].id
    }
  });

  return { subscriptionId: subscription.id };
}

Reporting Usage to Stripe

The key to metered billing is reporting usage to Stripe. You have two approaches:

Approach 1: Report Usage in Real-Time

Report each API call as it happens:

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

export async function reportUsage(
  userId: string,
  quantity: number,
  action: string
) {
  const user = await db.user.findUnique({
    where: { id: userId },
    select: { stripeSubscriptionItemId: true }
  });

  if (!user?.stripeSubscriptionItemId) {
    throw new Error('No active subscription');
  }

  // Report usage to Stripe
  const usageRecord = await stripe.subscriptionItems.createUsageRecord(
    user.stripeSubscriptionItemId,
    {
      quantity,
      timestamp: Math.floor(Date.now() / 1000),
      action: 'increment' // Add to existing usage
    }
  );

  // Also log locally for your own analytics
  await db.usageLog.create({
    data: {
      userId,
      quantity,
      action,
      stripeUsageRecordId: usageRecord.id
    }
  });

  return usageRecord;
}

Use it in your API routes:

// app/api/ai/generate/route.ts
import { NextResponse } from 'next/server';
import { auth } from '@/auth';

import { reportUsage } from '@/lib/usage';

export async function POST(request: Request) {
  const session = await auth();
  if (!session?.user?.id) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  try {
    // Process the AI generation
    const result = await generateWithAI(request);

    // Report 1 unit of usage
    await reportUsage(session.user.id, 1, 'ai_generation');

    return NextResponse.json(result);
  } catch (error) {
    return NextResponse.json({ error: 'Failed' }, { status: 500 });
  }
}

Approach 2: Batch Usage Reporting

For high-volume APIs, batch usage reports:

// lib/usage-batch.ts
import { db } from '@/lib/db';
import { stripe } from '@/lib/stripe';

// Track usage in memory or Redis
const usageBuffer = new Map<string, number>();

export function trackUsage(subscriptionItemId: string, quantity: number) {
  const current = usageBuffer.get(subscriptionItemId) || 0;
  usageBuffer.set(subscriptionItemId, current + quantity);
}

// Flush to Stripe periodically (e.g., every minute via cron)
export async function flushUsageToStripe() {
  const entries = Array.from(usageBuffer.entries());
  usageBuffer.clear();

  const results = await Promise.allSettled(
    entries.map(([subscriptionItemId, quantity]) =>
      stripe.subscriptionItems.createUsageRecord(subscriptionItemId, {
        quantity,
        timestamp: Math.floor(Date.now() / 1000),
        action: 'increment'
      })
    )
  );

  // Log any failures
  results.forEach((result, index) => {
    if (result.status === 'rejected') {
      console.error(
        `Failed to report usage for ${entries[index][0]}:`,
        result.reason
      );
      // Re-add to buffer for retry
      const [id, qty] = entries[index];
      trackUsage(id, qty);
    }
  });
}

Set up a cron job to flush usage:

// app/api/cron/flush-usage/route.ts
import { NextResponse } from 'next/server';

import { flushUsageToStripe } from '@/lib/usage-batch';

export async function GET(request: Request) {
  // Verify cron secret
  const authHeader = request.headers.get('authorization');
  if (authHeader !== `Bearer ${process.env.CRON_SECRET}`) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
  }

  await flushUsageToStripe();
  return NextResponse.json({ success: true });
}

Displaying Usage to Customers

Show customers their current usage:

// lib/usage.ts
export async function getCurrentUsage(userId: string) {
  const user = await db.user.findUnique({
    where: { id: userId },
    select: { stripeSubscriptionItemId: true, stripeSubscriptionId: true }
  });

  if (!user?.stripeSubscriptionId) {
    return null;
  }

  // Get current billing period usage from Stripe
  const subscription = await stripe.subscriptions.retrieve(
    user.stripeSubscriptionId
  );

  const usageSummary = await stripe.subscriptionItems.listUsageRecordSummaries(
    user.stripeSubscriptionItemId!,
    {
      limit: 1
    }
  );

  const currentPeriodUsage = usageSummary.data[0]?.total_usage || 0;

  return {
    usage: currentPeriodUsage,
    periodStart: new Date(subscription.current_period_start * 1000),
    periodEnd: new Date(subscription.current_period_end * 1000),
    estimatedCost: currentPeriodUsage * 0.01 // $0.01 per unit
  };
}

Create a usage dashboard component:

// components/usage-dashboard.tsx
import { getCurrentUsage } from '@/lib/usage';
import { formatCurrency, formatDate } from '@/lib/utils';

export async function UsageDashboard({ userId }: { userId: string }) {
  const usage = await getCurrentUsage(userId);

  if (!usage) {
    return <p>No active subscription</p>;
  }

  return (
    <div className="grid gap-4 md:grid-cols-3">
      <div className="rounded-lg border p-4">
        <p className="text-sm text-muted-foreground">Current Usage</p>
        <p className="text-3xl font-bold">{usage.usage.toLocaleString()}</p>
        <p className="text-sm text-muted-foreground">API calls</p>
      </div>

      <div className="rounded-lg border p-4">
        <p className="text-sm text-muted-foreground">Estimated Cost</p>
        <p className="text-3xl font-bold">
          {formatCurrency(usage.estimatedCost)}
        </p>
        <p className="text-sm text-muted-foreground">this period</p>
      </div>

      <div className="rounded-lg border p-4">
        <p className="text-sm text-muted-foreground">Billing Period</p>
        <p className="text-sm">
          {formatDate(usage.periodStart)} - {formatDate(usage.periodEnd)}
        </p>
      </div>
    </div>
  );
}

Setting Usage Limits

Prevent surprise bills by implementing usage limits:

// lib/usage-limits.ts
import { db } from '@/lib/db';

const USAGE_LIMITS = {
  free: 100,
  starter: 10000,
  pro: 100000,
  enterprise: Infinity
};

export async function checkUsageLimit(userId: string): Promise<{
  allowed: boolean;
  current: number;
  limit: number;
  remaining: number;
}> {
  const user = await db.user.findUnique({
    where: { id: userId },
    select: { plan: true, stripeSubscriptionItemId: true }
  });

  const limit = USAGE_LIMITS[user?.plan || 'free'];
  const usage = await getCurrentUsage(userId);
  const current = usage?.usage || 0;

  return {
    allowed: current < limit,
    current,
    limit,
    remaining: Math.max(0, limit - current)
  };
}

// Use in API routes
export async function POST(request: Request) {
  const session = await auth();
  const { allowed, remaining } = await checkUsageLimit(session.user.id);

  if (!allowed) {
    return NextResponse.json(
      { error: 'Usage limit exceeded', remaining },
      { status: 429 }
    );
  }

  // Process request...
}

Handling Webhooks for Metered Billing

Handle invoice events for metered subscriptions:

// app/api/webhooks/stripe/route.ts
case 'invoice.created': {
  const invoice = event.data.object as Stripe.Invoice;

  // For metered billing, the invoice is created at period end
  // with calculated usage charges
  console.log('Invoice created:', invoice.id);
  console.log('Total:', invoice.total);
  break;
}

case 'invoice.finalized': {
  const invoice = event.data.object as Stripe.Invoice;

  // Send usage summary email to customer
  await sendUsageSummaryEmail(invoice);
  break;
}

case 'invoice.payment_failed': {
  const invoice = event.data.object as Stripe.Invoice;

  // Handle failed payment (maybe pause API access)
  await handleFailedPayment(invoice);
  break;
}

Best Practices

1. Always Set Usage Alerts

Let customers know when they're approaching limits:

// Check usage and send alerts
export async function checkUsageAlerts(userId: string) {
  const { current, limit } = await checkUsageLimit(userId);
  const percentage = (current / limit) * 100;

  if (percentage >= 90 && !(await hasAlertBeenSent(userId, 90))) {
    await sendUsageAlert(userId, '90% of usage limit reached');
  } else if (percentage >= 75 && !(await hasAlertBeenSent(userId, 75))) {
    await sendUsageAlert(userId, '75% of usage limit reached');
  }
}

2. Provide Usage Estimates

Help customers predict costs:

export function estimateMonthlyCost(dailyUsage: number, pricePerUnit: number) {
  const estimatedMonthlyUsage = dailyUsage * 30;
  return estimatedMonthlyUsage * pricePerUnit;
}

3. Offer Committed Use Discounts

Reward customers who commit to minimum usage:

const VOLUME_DISCOUNTS = [
  { threshold: 100000, discount: 0.1 }, // 10% off above 100k
  { threshold: 500000, discount: 0.2 }, // 20% off above 500k
  { threshold: 1000000, discount: 0.3 } // 30% off above 1M
];

Conclusion

Metered billing with Stripe involves:

  1. Creating metered prices with usage_type: 'metered'
  2. Reporting usage via createUsageRecord
  3. Displaying usage to customers in real-time
  4. Setting limits to prevent surprise bills
  5. Handling webhooks for invoice events

This model works great for APIs, AI services, and any product where usage varies significantly between customers.


Want metered billing without building it yourself? Achromatic includes pre-built metered billing with usage tracking, limits, and customer dashboards out of the box.