Usage metering lets you track consumption of metered features (defined with type: "metered") and bill customers based on actual usage. Revstack handles meter storage, reset scheduling, and overage calculations automatically.
How Metering Works
Metered features follow this lifecycle:
Define metered feature
Configure the feature with type: "metered" and reset_period.
Check entitlement before usage
Verify the customer has capacity for the requested amount.
Report usage after consumption
Increment the customer’s usage meter with the actual amount consumed.
Automatic reset
Revstack resets meters at the end of each period (daily, weekly, monthly, etc.).
Defining Metered Features
Configure features with reset periods in your billing config:
import { defineFeature } from "@revstackhq/core" ;
export const features = {
api_calls: defineFeature ({
name: "API Calls" ,
type: "metered" ,
unit_type: "requests" ,
}),
ai_tokens: defineFeature ({
name: "AI Tokens" ,
description: "Combined input + output tokens" ,
type: "metered" ,
unit_type: "tokens" ,
}),
storage_bytes: defineFeature ({
name: "Storage" ,
type: "metered" ,
unit_type: "bytes" ,
}),
};
Then configure limits and reset periods in your plans:
import { definePlan } from "@revstackhq/core" ;
import { features } from "./features" ;
export const plans = {
pro: definePlan < typeof features >({
name: "Pro" ,
type: "paid" ,
prices: [
{
amount: 2900 ,
currency: "USD" ,
billing_interval: "monthly" ,
overage_configuration: {
api_calls: {
overage_amount: 10 , // $0.10 in cents
overage_unit: 1000 , // per 1,000 calls
},
ai_tokens: {
overage_amount: 15 ,
overage_unit: 1000000 , // per 1M tokens
},
},
},
],
features: {
api_calls: {
value_limit: 100000 , // 100K included
is_hard_limit: false , // Allow overage
reset_period: "monthly" ,
},
ai_tokens: {
value_limit: 10000000 , // 10M included
is_hard_limit: false ,
reset_period: "monthly" ,
},
storage_bytes: {
value_limit: 10737418240 , // 10 GB
is_hard_limit: true , // Block when full
reset_period: "never" , // Doesn't reset
},
},
}),
};
Reset Periods
daily : Resets at midnight UTC every day
weekly : Resets at midnight UTC every Sunday
monthly : Resets at midnight UTC on the 1st of each month
yearly : Resets at midnight UTC on January 1st
never : Never resets (useful for storage quotas)
Revstack handles reset scheduling automatically. You don’t need to run cron jobs or background workers.
Reporting Usage
Use the Usage Client to report consumption:
import { Revstack } from "@revstackhq/node" ;
const revstack = new Revstack ({ secretKey: process . env . REVSTACK_SECRET_KEY });
// Report API call usage
await revstack . usage . report ({
customerId: "usr_abc123" ,
featureId: "api_calls" ,
amount: 1 ,
});
// Report AI token usage
await revstack . usage . report ({
customerId: "usr_abc123" ,
featureId: "ai_tokens" ,
amount: 5420 , // Actual tokens consumed
idempotencyKey: `req_ ${ requestId } ` , // Prevent double-counting
});
// Report storage usage
await revstack . usage . report ({
customerId: "usr_abc123" ,
featureId: "storage_bytes" ,
amount: 1048576 , // 1 MB uploaded
});
Idempotency Keys
Use idempotency keys to prevent double-counting if your request is retried:
await revstack . usage . report ({
customerId: userId ,
featureId: "api_calls" ,
amount: 1 ,
idempotencyKey: `req_ ${ requestId } ` , // Same key = idempotent
});
Always use idempotency keys for production workloads. Network failures or retries can lead to duplicate usage reports.
Querying Usage Meters
Retrieve current usage for a customer:
// Get all meters for a customer
const meters = await revstack . usage . getMeters ( "usr_abc123" );
console . log ( meters );
// [
// {
// featureId: 'api_calls',
// currentUsage: 45230,
// limit: 100000,
// resetAt: '2026-04-01T00:00:00Z',
// resetPeriod: 'monthly',
// },
// {
// featureId: 'ai_tokens',
// currentUsage: 8500000,
// limit: 10000000,
// resetAt: '2026-04-01T00:00:00Z',
// resetPeriod: 'monthly',
// },
// ]
// Get a single meter
const apiCallsMeter = await revstack . usage . getMeter ( "usr_abc123" , "api_calls" );
console . log ( apiCallsMeter );
// {
// featureId: 'api_calls',
// currentUsage: 45230,
// limit: 100000,
// remaining: 54770,
// resetAt: '2026-04-01T00:00:00Z',
// resetPeriod: 'monthly',
// }
Reverting Usage
If an operation fails after you’ve already reported usage, revert it:
// Optimistic usage pattern
await revstack . usage . report ({
customerId: userId ,
featureId: "ai_tokens" ,
amount: 5000 ,
});
try {
const result = await openai . chat . completions . create ({ /* ... */ });
} catch ( error ) {
// Revert usage if the operation failed
await revstack . usage . revert ({
customerId: userId ,
featureId: "ai_tokens" ,
amount: 5000 ,
reason: "operation_failed" ,
});
throw error ;
}
The reason field is stored for audit trails. Use descriptive reasons like "operation_failed", "refund_issued", or "duplicate_request".
Hard vs Soft Limits
Hard Limits (Strict Quotas)
Block access when the limit is reached:
features : {
storage_bytes : {
value_limit : 10737418240 , // 10 GB
is_hard_limit : true , // No overage allowed
reset_period : "never" ,
},
}
Entitlement check behavior :
const check = await revstack . entitlements . check ( userId , "storage_bytes" , { amount: 1048576 });
if ( check . allowed ) {
await uploadFile ( file );
await revstack . usage . report ({ customerId: userId , featureId: "storage_bytes" , amount: file . size });
} else {
// check.reason === 'limit_reached'
throw new Error ( "Storage quota exceeded. Please upgrade your plan." );
}
Soft Limits (Overage Allowed)
Allow usage beyond the included limit with additional charges:
features : {
api_calls : {
value_limit : 100000 ,
is_hard_limit : false , // Allow overage
reset_period : "monthly" ,
},
}
// Configure overage pricing in the plan
prices : [
{
amount: 2900 ,
currency: "USD" ,
billing_interval: "monthly" ,
overage_configuration: {
api_calls: {
overage_amount: 10 , // $0.10 per 1,000 calls
overage_unit: 1000 ,
},
},
},
]
Entitlement check behavior :
const check = await revstack . entitlements . check ( userId , "api_calls" , { amount: 1 });
if ( check . allowed ) {
if ( check . reason === "overage_allowed" ) {
// User is beyond their included limit - will be charged extra
console . log ( `Overage cost estimate: $ ${ check . cost_estimate / 100 } ` );
}
await processApiCall ();
await revstack . usage . report ({ customerId: userId , featureId: "api_calls" , amount: 1 });
}
Important : The Entitlement Engine determines if overage is allowed (is_hard_limit: false). The overage_configuration in your plan’s prices determines how much to charge.
Common Patterns
Optimistic Usage Reporting
For low-latency operations, report usage immediately after the entitlement check:
// 1. Check entitlement (fast)
const check = await revstack . entitlements . check ( userId , "ai_tokens" , { amount: 5000 });
if ( ! check . allowed ) throw new Error ( "Token limit exceeded" );
// 2. Report estimated usage immediately (don't wait)
await revstack . usage . report ({
customerId: userId ,
featureId: "ai_tokens" ,
amount: 5000 ,
});
// 3. Call external API (slow)
const result = await openai . chat . completions . create ({ max_tokens: 5000 , /* ... */ });
// 4. Adjust if actual usage differs (optional)
const actualUsage = result . usage . total_tokens ;
if ( actualUsage !== 5000 ) {
await revstack . usage . revert ({
customerId: userId ,
featureId: "ai_tokens" ,
amount: 5000 ,
reason: "adjustment" ,
});
await revstack . usage . report ({
customerId: userId ,
featureId: "ai_tokens" ,
amount: actualUsage ,
});
}
Batch Usage Reporting
For high-throughput workloads, batch reports to reduce API calls:
// Accumulate usage in memory
const usageBuffer : Map < string , number > = new Map ();
function trackUsage ( customerId : string , amount : number ) {
const current = usageBuffer . get ( customerId ) || 0 ;
usageBuffer . set ( customerId , current + amount );
}
// Flush periodically (e.g., every 10 seconds)
setInterval ( async () => {
const reports = Array . from ( usageBuffer . entries ()). map (([ customerId , amount ]) => ({
customerId ,
featureId: "api_calls" ,
amount ,
}));
await Promise . all ( reports . map (( r ) => revstack . usage . report ( r )));
usageBuffer . clear ();
}, 10000 );
Tradeoff : Batching improves performance but delays usage updates. Entitlement checks may be stale if the meter hasn’t been flushed yet.
Displaying Usage to Users
Show current usage in your dashboard:
import { useState , useEffect } from "react" ;
import { Revstack } from "@revstackhq/node" ;
function UsageDashboard ({ userId } : { userId : string }) {
const [ meters , setMeters ] = useState < any []>([]);
useEffect (() => {
async function fetchUsage () {
const revstack = new Revstack ({ secretKey: process . env . REVSTACK_SECRET_KEY });
const data = await revstack . usage . getMeters ( userId );
setMeters ( data );
}
fetchUsage ();
}, [ userId ]);
return (
< div >
{ meters . map (( meter ) => {
const percentUsed = ( meter . currentUsage / meter . limit ) * 100 ;
const resetDate = new Date ( meter . resetAt ). toLocaleDateString ();
return (
< div key = { meter . featureId } >
< h3 > { meter . featureId } </ h3 >
< ProgressBar value = { percentUsed } />
< p >
{ meter . currentUsage . toLocaleString () } / { meter . limit . toLocaleString () }
{ " " } ( { percentUsed . toFixed ( 1 ) } % used)
</ p >
< p > Resets on { resetDate } </ p >
</ div >
);
}) }
</ div >
);
}
Proactive Overage Notifications
Warn users before they hit overage:
const check = await revstack . entitlements . check ( userId , "api_calls" , { amount: 1 });
if ( check . remaining !== undefined && check . remaining < 1000 ) {
// User has less than 1,000 calls remaining
await sendEmail ({
to: user . email ,
subject: "API quota running low" ,
body: `You have ${ check . remaining } API calls remaining this month.` ,
});
}
Billing Workflow
Revstack handles overage billing automatically:
During the period : Usage is tracked and accumulated
At period end : Revstack calculates overage charges based on overage_configuration
Invoice generation : Overage is added to the next invoice
Meter reset : Usage resets to 0 for the new period
Overage charges are invoiced at the end of the billing period, not in real-time. This matches how providers like AWS and Stripe handle usage-based billing.
Next Steps
Subscriptions Learn how subscriptions handle recurring billing
Providers Understand payment provider integrations