Why usage-based billing matters
Most SaaS platforms charge a flat monthly fee, but what if your customer's value scales unpredictably? A video processing API might see one customer process 100 minutes per month and another burn through 100,000. A flat tier doesn't capture that difference fairly. Usage-based billing: a practical Payments & Stripe guide shows you how to charge for what customers actually use, align revenue with delivered value, and build pricing that scales with your users' success.
In this guide, we'll walk through the mechanics of metering usage events, aggregating them over billing periods, submitting them safely to Stripe, and surfacing real-time consumption data to your customers. You've probably handled events before—this is similar, but with the finality of billing attached.
Meters record usage events
A meter is your first line of defense against billing errors. Every time a customer performs a billable action—process a video, send an SMS, query a database—you emit an event to your meter. The meter doesn't immediately charge; it records the event and holds a running total until the billing cycle closes.
In Stripe's world, meters are created in your dashboard or via API and associated with a billing meter product. You then push usage events to that meter with a customer_id, timestamp, and quantity. Let's say you run a document-processing service and charge per page processed:
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
async function recordPageProcessed(customerId: string, pageCount: number) {
try {
const event = await stripe.billing.meters.events.create({
meter_event_name: 'pages_processed',
identifier: customerId,
quantity: pageCount,
timestamp: Math.floor(Date.now() / 1000),
});
console.log(`Recorded ${pageCount} pages for ${customerId}`);
return event;
} catch (error) {
console.error('Failed to record usage:', error);
throw error;
}
}
In your application, you'd call this function whenever a document is processed. For a web service, this might live in your request handler; for async work, it could be in a job completion callback. The key is to emit the event as close to the action as possible, so your data reflects reality.
Stripe's billing meters are designed to handle high-volume events. They're idempotent by default (more on that later), and they automatically deduplicate on the same second for the same customer and meter. If your job processor records the same event twice within a second, only one counts.
Aggregate per billing period
Stripe aggregates your metered events automatically during each billing cycle. When a subscription renews, Stripe sums up all usage events for that meter over the billing period and calculates the invoice amount based on your meter's pricing.
For example, if you charge $0.05 per page and a customer processed 2,000 pages in a month, Stripe will invoice them $100 for that meter's line item. You don't need to manually sum or calculate; Stripe does it. Your pricing configuration in Stripe determines how the quantity is converted to currency.
Set up your meter in the Stripe dashboard under Billing > Meters, then create a product with a price that uses that meter. Here's the essential setup:
| Field | Example | Purpose |
|---|---|---|
| Meter name | pages_processed | Identifier for the meter in your API calls |
| Display name | Pages Processed | Human-readable name on invoices |
| Aggregation formula | Sum (or max/last value) | How usage is aggregated per period |
| Price per unit | $0.05 | Multiplied by total usage quantity |
You can also mix metered and fixed charges. A customer might pay $29/month for the service plus $0.05 per page. On the Stripe side, that's two separate price objects attached to the same subscription.
Send usage idempotently
Network failures, retries, and duplicate events are inevitable. If your job processor crashes after recording usage but before updating its own state, it might replay the same event. Idempotency ensures that replaying doesn't double-bill your customer.
Stripe's meter events endpoint supports an idempotency_key parameter. Use a deterministic key based on the event's essence—not a random UUID:
import crypto from 'crypto';
async function recordPageProcessedIdempotent(
customerId: string,
jobId: string,
pageCount: number
) {
// Create a deterministic key from job ID and customer
const idempotencyKey = crypto
.createHash('sha256')
.update(`${jobId}:${customerId}:pages`)
.digest('hex');
try {
const event = await stripe.billing.meters.events.create(
{
meter_event_name: 'pages_processed',
identifier: customerId,
quantity: pageCount,
timestamp: Math.floor(Date.now() / 1000),
},
{
idempotencyKey,
}
);
console.log(`Recorded usage for job ${jobId}`);
return event;
} catch (error) {
console.error('Failed to record usage:', error);
throw error;
}
}
The idempotency key should be unique per real-world event (one job completion = one key). If the same key hits Stripe twice, the second request returns the same response as the first. Stripe keeps idempotency keys for 24 hours.
In production, pair this with an outbox pattern: log the usage event to your database before sending it to Stripe, then asynchronously poll and retry. Only mark the event as sent after Stripe confirms it. This way, if your service crashes mid-request, you'll retry and the idempotency key saves you.
Show usage in your dashboard
Customers hate surprises. Surfacing real-time usage in your application builds trust and helps them understand their bill. You should display:
- Current billing period usage (e.g., "1,847 / 5,000 pages this month")
- Current accrued cost (e.g., "Current charges: $92.35")
- Historical usage and invoices
- Projected end-of-month bill if usage continues at current rate
You have two options: query Stripe's API for the data, or maintain your own cache.
Option 1: Query Stripe directly is simpler but slower. You can fetch the customer's current subscription and check for recent invoices, but Stripe's API doesn't expose real-time meter aggregates until the invoice is issued.
Option 2: Track usage locally is recommended for production. Store each usage event in your database as you emit it, then aggregate on read. This lets you show customers accurate, real-time consumption without API latency:
import { db } from './database';
// After calling recordPageProcessedIdempotent
async function recordUsageLocally(
customerId: string,
pageCount: number,
timestamp: Date
) {
await db.usageEvents.insert({
customer_id: customerId,
meter_name: 'pages_processed',
quantity: pageCount,
recorded_at: timestamp,
synced_to_stripe: true,
});
}
async function getCurrentMonthUsage(customerId: string) {
const now = new Date();
const monthStart = new Date(now.getFullYear(), now.getMonth(), 1);
const usage = await db.usageEvents.query
.where('customer_id', customerId)
.where('meter_name', 'pages_processed')
.where('recorded_at', '>=', monthStart)
.select('SUM(quantity) as total');
return usage[0]?.total || 0;
}
async function getProjectedMonthlyBill(customerId: string, pricePerUnit: number) {
const monthStart = new Date(new Date().getFullYear(), new Date().getMonth(), 1);
const monthEnd = new Date(new Date().getFullYear(), new Date().getMonth() + 1, 0);
const daysInMonth = monthEnd.getDate();
const daysPassed = new Date().getDate();
const currentUsage = await getCurrentMonthUsage(customerId);
const projectedUsage = (currentUsage / daysPassed) * daysInMonth;
return Math.ceil(projectedUsage * pricePerUnit * 100) / 100;
}
Display this on a dashboard endpoint that customers can visit. Include a disclaimer that the projection is informational and the final bill may differ slightly depending on actual usage patterns.
Common mistakes
- Recording usage without idempotency keys: A retry in your infrastructure or Stripe's network becomes a double charge. Always use deterministic keys.
- Assuming Stripe aggregates in real time: Stripe only aggregates and invoices at the billing period boundary. Don't rely on fetching the current month's total from Stripe; track it yourself.
- Storing idempotency keys in memory: If your service restarts, you lose the key and can't deduplicate. Log it to a database or pass it through Stripe's API.
- Emitting usage events after invoice generation: Once Stripe generates an invoice, it won't accept new meter events for that period. Record usage promptly or use a delayed emission strategy with a buffer.
- Ignoring meter event errors silently: If a meter event fails to reach Stripe (network timeout, auth failure), your local cache and Stripe will diverge. Log and alert on failures; don't swallow them.
Key takeaways
- Emit usage events as close as possible to the action that triggers them, using deterministic, meter-specific event names.
- Use idempotency keys based on the job or action ID, not random values, to survive retries without double-billing.
- Trust Stripe's aggregation for the official invoice, but track usage locally for real-time dashboard display and projection.
- Always show customers their current usage, accrued cost, and projected end-of-period bill to build trust and reduce billing disputes.
- Pair meter events with your existing outbox and retry logic to ensure data consistency between your system and Stripe.






Comments
Loading comments…