MeterEngine
Usage tracking and budget enforcement system for metering entity activity.
Prerequisites: projection-engine.md
Overview
Section titled “Overview”The MeterEngine tracks usage per entity per time window and enforces budget limits. It’s purpose-built for:
- Usage metering - Track API calls, compute minutes, storage bytes, bandwidth
- Budget enforcement - Reject requests when limits are exceeded
- Cost management - Calculate overage charges when budgets are exceeded
- Proactive alerts - Warn before reaching limits using ThresholdMonitor
MeterEngine supports:
- 3 time windows - hour, day, month
- 2 aggregation types - count (number of events), sum (total value)
- Budget limits - Hard limits with automatic enforcement
- Warning thresholds - Early warnings before hitting limits
- Overage pricing - Allow overages with per-unit cost
import { parseMeterConfig, MeterEngine } from '@z0-app/sdk';
const yaml = `source: entityfactTypes: [api_call]unit: requestsaggregation: countwindows: [hour, day, month]budgetCheck: limit: 1000 window: day warningThreshold: 800 overage: enabled: true unit_price_cents: 50`;
const config = parseMeterConfig(yaml);const meter = new MeterEngine('api_meter', config);
// Track usagemeter.incrementUsage('customer_123', Date.now(), 5);
// Check budget before allowing operationconst result = meter.checkBudget('customer_123', Date.now(), 10);if (result.allowed) { console.log(`Allowed. Remaining: ${result.remaining}`); if (result.warning) { console.warn('Approaching budget limit!'); } if (result.overage) { console.log(`Overage cost: $${result.overage.cost_cents / 100}`); }} else { console.log(`Rejected. Retry at: ${new Date(result.retry_at)}`);}Configuration
Section titled “Configuration”MeterEngine uses YAML configuration parsed via parseMeterConfig().
Signature
Section titled “Signature”function parseMeterConfig(yaml: string): MeterConfig
interface MeterConfig { source: string; // Source entity type factTypes: string[]; // Fact types to meter unit: string; // Unit of measurement (e.g., 'requests', 'bytes') aggregation: MeterAggregation; // How to aggregate usage windows: MeterWindow[]; // Time windows to track budgetCheck?: BudgetCheckConfig; // Optional budget enforcement}
type MeterWindow = 'hour' | 'day' | 'month';type MeterAggregation = 'count' | 'sum';
interface BudgetCheckConfig { limit: number; // Budget limit window: MeterWindow; // Window for enforcement warningThreshold?: number; // Optional warning level overage?: OverageConfig; // Optional overage pricing}
interface OverageConfig { enabled: boolean; // Allow overages? unit_price_cents: number; // Cost per unit over limit}Error Handling
Section titled “Error Handling”import { parseMeterConfig, ConfigParseError, ConfigValidationError} from '@z0-app/sdk';
try { const config = parseMeterConfig(yamlString);} catch (err) { if (err instanceof ConfigParseError) { console.error('Invalid YAML syntax:', err.message); } else if (err instanceof ConfigValidationError) { console.error('Config validation failed:', err.message); console.error('Issues:', err.issues); // Array of specific errors }}Usage Tracking
Section titled “Usage Tracking”Incrementing Usage
Section titled “Incrementing Usage”Use incrementUsage() to atomically record usage across all configured windows:
const meter = new MeterEngine('api_calls', config);
// Record usage for an entitymeter.incrementUsage( 'customer_123', // Entity ID Date.now(), // Timestamp 5 // Amount to increment);Behavior:
- Increments usage in all configured windows atomically
- Windows are calculated from timestamp
- Usage persists in memory (production systems should persist to storage)
Querying Usage
Section titled “Querying Usage”Use getUsage() to retrieve current usage for a specific window:
const hourlyUsage = meter.getUsage('customer_123', 'hour');const dailyUsage = meter.getUsage('customer_123', 'day');const monthlyUsage = meter.getUsage('customer_123', 'month');
console.log(`Usage: ${hourlyUsage} this hour, ${dailyUsage} today`);Parameters:
entityId- Entity identifierwindow- Time window to query (‘hour’ | ‘day’ | ‘month’)timestamp- Optional reference timestamp (defaults toDate.now())
Returns: Current usage count for the window, or 0 if no usage recorded.
Example: Track API Calls
Section titled “Example: Track API Calls”const config = parseMeterConfig(`source: apifactTypes: [request_completed]unit: requestsaggregation: countwindows: [hour, day]`);
const meter = new MeterEngine('api_calls', config);
// Record requestsmeter.incrementUsage('customer_123', Date.now(), 1);meter.incrementUsage('customer_123', Date.now(), 1);meter.incrementUsage('customer_123', Date.now(), 1);
// Query usageconst hourly = meter.getUsage('customer_123', 'hour');const daily = meter.getUsage('customer_123', 'day');
console.log(`${hourly} requests this hour, ${daily} today`);// Output: "3 requests this hour, 3 today"Budget Enforcement
Section titled “Budget Enforcement”Checking Budget
Section titled “Checking Budget”Use checkBudget() before allowing operations to enforce budget limits:
const result = meter.checkBudget( 'customer_123', // Entity ID Date.now(), // Reference timestamp 10 // Requested amount);
if (result.allowed) { // Proceed with operation console.log(`Remaining budget: ${result.remaining}`);} else { // Reject and provide retry time console.log(`Budget exceeded. Retry at: ${new Date(result.retry_at)}`);}Allowed Result
Section titled “Allowed Result”When usage is within budget limits:
interface BudgetCheckAllowed { allowed: true; remaining?: number; // Units remaining before limit warning?: 'approaching_limit'; // Warning if near threshold overage?: OverageDetails; // Overage details if applicable}
interface OverageDetails { count: number; // Units over the limit cost_cents: number; // Cost in cents}Example:
const result = meter.checkBudget('customer_123', Date.now(), 50);
if (result.allowed) { console.log(`Allowed. Remaining: ${result.remaining} units`);
if (result.warning === 'approaching_limit') { console.warn('Warning: Approaching budget limit!'); }
if (result.overage) { console.log( `Overage: ${result.overage.count} units, ` + `cost $${result.overage.cost_cents / 100}` ); }}Rejected Result
Section titled “Rejected Result”When budget is exceeded (and overage is disabled):
interface BudgetCheckRejected { allowed: false; reason: 'budget_exceeded'; retry_at: number; // Timestamp when next window starts}Example:
const result = meter.checkBudget('customer_123', Date.now(), 100);
if (!result.allowed) { const retryDate = new Date(result.retry_at); console.log(`Rejected: ${result.reason}. Retry at ${retryDate}`); // Output: "Rejected: budget_exceeded. Retry at 2026-03-13T00:00:00.000Z"}Budget Configuration Options
Section titled “Budget Configuration Options”Basic Budget Limit
Section titled “Basic Budget Limit”Enforce a hard limit without warnings or overage:
source: apifactTypes: [request_completed]unit: requestsaggregation: countwindows: [day]budgetCheck: limit: 1000 window: day// After 1000 requests, checkBudget returns allowed: falseWarning Threshold
Section titled “Warning Threshold”Alert when approaching limit (integrates with ThresholdMonitor):
budgetCheck: limit: 1000 window: day warningThreshold: 800 # Warn when 800+ usedconst result = meter.checkBudget('customer_123', Date.now(), 50);
if (result.warning === 'approaching_limit') { // Send alert email, log to monitoring, etc. console.warn('Customer approaching daily budget limit!');}ThresholdMonitor Integration:
- Uses
ThresholdMonitorinternally for debounced alerts - Only triggers warning once when crossing threshold
- Resets after budget window rolls over
Overage Pricing
Section titled “Overage Pricing”Allow usage beyond limit with per-unit pricing:
budgetCheck: limit: 1000 window: day overage: enabled: true unit_price_cents: 50 # $0.50 per unit over limit// Customer uses 1050 requestsconst result = meter.checkBudget('customer_123', Date.now(), 0);
console.log(result);// {// allowed: true,// remaining: 0,// overage: {// count: 50,// cost_cents: 2500 // 50 * 50 = 2500 cents = $25.00// }// }Time Windows
Section titled “Time Windows”MeterEngine tracks usage across configured time windows, automatically resetting when windows roll over.
Window Behavior
Section titled “Window Behavior”| Window | Format | Reset Behavior |
|---|---|---|
hour | YYYY-MM-DDTHH | Resets at top of next hour (UTC) |
day | YYYY-MM-DD | Resets at midnight UTC |
month | YYYY-MM | Resets on 1st of next month (UTC) |
All timestamps use UTC timezone.
Example: Window Rollover
Section titled “Example: Window Rollover”const config = parseMeterConfig(`source: apifactTypes: [request]unit: requestsaggregation: countwindows: [day]budgetCheck: limit: 100 window: day`);
const meter = new MeterEngine('daily_limit', config);
// March 12, 2026 - 95 requestsconst march12 = Date.UTC(2026, 2, 12, 22, 0, 0);meter.incrementUsage('customer_123', march12, 95);
// Check usage on March 12const usage12 = meter.getUsage('customer_123', 'day', march12);console.log(usage12); // 95
// March 13, 2026 - new day, new windowconst march13 = Date.UTC(2026, 2, 13, 2, 0, 0);const usage13 = meter.getUsage('customer_123', 'day', march13);console.log(usage13); // 0 (reset)
// Budget enforcement uses the window from budgetCheck.windowconst result = meter.checkBudget('customer_123', march13, 50);console.log(result.allowed); // true (new window)Multiple Windows
Section titled “Multiple Windows”Track usage across multiple windows simultaneously:
source: apifactTypes: [request]unit: requestsaggregation: countwindows: [hour, day, month] # Track all threebudgetCheck: limit: 10000 window: month # Enforce monthly limitconst meter = new MeterEngine('multi_window', config);
meter.incrementUsage('customer_123', Date.now(), 50);
// Query each windowconst hourly = meter.getUsage('customer_123', 'hour');const daily = meter.getUsage('customer_123', 'day');const monthly = meter.getUsage('customer_123', 'month');
console.log(`Usage: ${hourly}/hr, ${daily}/day, ${monthly}/month`);Aggregation Types
Section titled “Aggregation Types”count - Event Counting
Section titled “count - Event Counting”Count the number of times an event occurs:
source: apifactTypes: [request_completed]unit: requestsaggregation: countwindows: [day]budgetCheck: limit: 1000 window: day// Each increment adds 1meter.incrementUsage('customer_123', Date.now(), 1);meter.incrementUsage('customer_123', Date.now(), 1);
const usage = meter.getUsage('customer_123', 'day');console.log(usage); // 2sum - Value Accumulation
Section titled “sum - Value Accumulation”Sum numeric values (bytes, minutes, tokens, etc.):
source: storagefactTypes: [file_uploaded, file_deleted]unit: bytesaggregation: sumwindows: [day, month]budgetCheck: limit: 10000000 # 10 MB daily limit window: day// Track file uploads by sizemeter.incrementUsage('customer_123', Date.now(), 2500000); // 2.5 MBmeter.incrementUsage('customer_123', Date.now(), 1500000); // 1.5 MB
const usage = meter.getUsage('customer_123', 'day');console.log(usage); // 4000000 (4 MB)
// Check if next upload would exceed limitconst result = meter.checkBudget('customer_123', Date.now(), 7000000); // 7 MBconsole.log(result.allowed); // false (4 + 7 > 10)ThresholdMonitor Integration
Section titled “ThresholdMonitor Integration”MeterEngine uses ThresholdMonitor internally for warning threshold detection with automatic debounce.
How It Works
Section titled “How It Works”When warningThreshold is configured:
- Before each budget check, MeterEngine calculates remaining budget
- ThresholdMonitor.check() compares old remaining vs. new remaining
- If crossing threshold, sets
warning: 'approaching_limit' - Debounce prevents repeated warnings until budget resets or usage decreases
Example: Proactive Alerts
Section titled “Example: Proactive Alerts”const config = parseMeterConfig(`source: apifactTypes: [request]unit: requestsaggregation: countwindows: [day]budgetCheck: limit: 1000 window: day warningThreshold: 800`);
const meter = new MeterEngine('api_meter', config);const entityId = 'customer_123';
// Usage builds upmeter.incrementUsage(entityId, Date.now(), 700);
// Check budget - no warning yetlet result = meter.checkBudget(entityId, Date.now(), 50);console.log(result.warning); // undefined (750 < 800)
// Increment to cross thresholdmeter.incrementUsage(entityId, Date.now(), 100);
// Check budget - warning triggeredresult = meter.checkBudget(entityId, Date.now(), 10);console.log(result.warning); // 'approaching_limit'
// Send alertif (result.warning === 'approaching_limit') { sendAlertEmail(entityId, 'You are approaching your daily API limit');}
// Subsequent checks are debounced (no duplicate alerts)result = meter.checkBudget(entityId, Date.now(), 5);console.log(result.warning); // undefined (debounced)Key Benefits:
- No duplicate alerts - ThresholdMonitor prevents re-triggering
- Automatic reset - Debounce clears when budget window resets
- Recovery detection - Can detect when usage drops back below threshold
Complete Examples
Section titled “Complete Examples”Example 1: API Rate Limiting
Section titled “Example 1: API Rate Limiting”Enforce per-customer API request limits:
source: api_gatewayfactTypes: [request_completed]unit: requestsaggregation: countwindows: [hour, day]budgetCheck: limit: 10000 window: day warningThreshold: 8000const config = parseMeterConfig(yaml);const meter = new MeterEngine('api_rate_limit', config);
// Middleware functionasync function apiMiddleware(req, res, next) { const customerId = req.user.id;
// Check budget before processing request const result = meter.checkBudget(customerId, Date.now(), 1);
if (!result.allowed) { return res.status(429).json({ error: 'Rate limit exceeded', retry_after: new Date(result.retry_at).toISOString(), }); }
// Warn if approaching limit if (result.warning === 'approaching_limit') { res.setHeader('X-RateLimit-Warning', 'Approaching daily limit'); }
// Process request await handleRequest(req, res);
// Record usage after successful request meter.incrementUsage(customerId, Date.now(), 1);
// Add rate limit headers res.setHeader('X-RateLimit-Remaining', result.remaining);}Example 2: Storage Quota with Overage
Section titled “Example 2: Storage Quota with Overage”Track storage usage with soft limits and overage billing:
source: storagefactTypes: [file_uploaded]unit: bytesaggregation: sumwindows: [month]budgetCheck: limit: 10737418240 # 10 GB window: month warningThreshold: 9663676416 # 9 GB overage: enabled: true unit_price_cents: 10 # $0.10 per GB over limitconst config = parseMeterConfig(yaml);const meter = new MeterEngine('storage_quota', config);
async function uploadFile(customerId, fileSize) { // Check budget const result = meter.checkBudget(customerId, Date.now(), fileSize);
if (!result.allowed) { throw new Error(`Storage quota exceeded. Resets on ${new Date(result.retry_at)}`); }
// Warn if approaching limit if (result.warning === 'approaching_limit') { console.warn(`Customer ${customerId} approaching storage quota`); await sendEmail(customerId, 'storage_warning'); }
// Calculate overage cost if (result.overage) { const overageGB = result.overage.count / (1024 ** 3); const cost = result.overage.cost_cents / 100; console.log(`Overage: ${overageGB.toFixed(2)} GB = $${cost.toFixed(2)}`);
// Record overage charge await recordCharge(customerId, 'storage_overage', cost); }
// Upload file await performUpload(fileSize);
// Record usage meter.incrementUsage(customerId, Date.now(), fileSize);}Example 3: Compute Minutes Tracking
Section titled “Example 3: Compute Minutes Tracking”Track compute usage with budget enforcement:
source: computefactTypes: [job_completed]unit: minutesaggregation: sumwindows: [day, month]budgetCheck: limit: 10000 window: month warningThreshold: 8000const config = parseMeterConfig(yaml);const meter = new MeterEngine('compute_usage', config);
async function runJob(customerId, estimatedMinutes) { // Pre-flight check const preCheck = meter.checkBudget(customerId, Date.now(), estimatedMinutes);
if (!preCheck.allowed) { throw new Error(`Insufficient compute budget. Resets ${new Date(preCheck.retry_at)}`); }
// Run job const startTime = Date.now(); await executeJob(); const endTime = Date.now();
// Calculate actual minutes const actualMinutes = (endTime - startTime) / (1000 * 60);
// Record actual usage meter.incrementUsage(customerId, Date.now(), actualMinutes);
// Post-job check for alerts const postCheck = meter.checkBudget(customerId, Date.now(), 0); if (postCheck.warning === 'approaching_limit') { await notifyCustomer(customerId, 'compute_warning', { used: 10000 - (postCheck.remaining ?? 0), limit: 10000, remaining: postCheck.remaining, }); }
return { actualMinutes, remaining: postCheck.remaining };}Example 4: Multi-Meter System
Section titled “Example 4: Multi-Meter System”Use multiple meters per entity for different resource types:
// API rate limiterconst apiConfig = parseMeterConfig(`source: apifactTypes: [request]unit: requestsaggregation: countwindows: [hour, day]budgetCheck: limit: 1000 window: hour`);const apiMeter = new MeterEngine('api_calls', apiConfig);
// Bandwidth trackerconst bandwidthConfig = parseMeterConfig(`source: networkfactTypes: [data_transfer]unit: bytesaggregation: sumwindows: [day, month]budgetCheck: limit: 107374182400 # 100 GB/month window: month`);const bandwidthMeter = new MeterEngine('bandwidth', bandwidthConfig);
// Storage quotaconst storageConfig = parseMeterConfig(`source: storagefactTypes: [file_operation]unit: bytesaggregation: sumwindows: [month]budgetCheck: limit: 10737418240 # 10 GB window: month`);const storageMeter = new MeterEngine('storage', storageConfig);
// Check all meters before operationasync function checkAllLimits(customerId, requestSize) { const apiCheck = apiMeter.checkBudget(customerId, Date.now(), 1); const bandwidthCheck = bandwidthMeter.checkBudget(customerId, Date.now(), requestSize); const storageCheck = storageMeter.checkBudget(customerId, Date.now(), requestSize);
if (!apiCheck.allowed) { throw new Error(`API rate limit exceeded. Retry at ${new Date(apiCheck.retry_at)}`); } if (!bandwidthCheck.allowed) { throw new Error(`Bandwidth quota exceeded. Retry at ${new Date(bandwidthCheck.retry_at)}`); } if (!storageCheck.allowed) { throw new Error(`Storage quota exceeded. Retry at ${new Date(storageCheck.retry_at)}`); }
return { apiCheck, bandwidthCheck, storageCheck };}Best Practices
Section titled “Best Practices”1. Check Budget Before Incrementing
Section titled “1. Check Budget Before Incrementing”Always check budget before performing the operation:
// ✅ GOOD - Check before operationconst result = meter.checkBudget(customerId, Date.now(), requestedAmount);if (result.allowed) { await performOperation(); meter.incrementUsage(customerId, Date.now(), requestedAmount);}
// ❌ BAD - Increment then check (operation already happened)meter.incrementUsage(customerId, Date.now(), requestedAmount);const result = meter.checkBudget(customerId, Date.now(), 0);2. Use Warning Thresholds for UX
Section titled “2. Use Warning Thresholds for UX”Set warningThreshold to alert users before hard limit:
budgetCheck: limit: 1000 window: day warningThreshold: 800 # Alert at 80%Benefits:
- Users have time to upgrade or reduce usage
- Prevents surprise rejections
- Better customer experience
3. Choose Appropriate Windows
Section titled “3. Choose Appropriate Windows”Match window to use case:
- hour - Real-time rate limiting (burst protection)
- day - Daily quotas (API calls, compute minutes)
- month - Billing cycles (storage, bandwidth)
4. Persist MeterEngine State
Section titled “4. Persist MeterEngine State”In production, persist MeterEngine usage data to Durable Object storage or database:
class MeterDO extends DurableObject { private meter: MeterEngine;
async fetch(request: Request) { // Load state from storage on first access if (!this.meter) { const state = await this.ctx.storage.get('meter_state'); this.meter = MeterEngine.fromState(state); }
// Handle request...
// Persist state after changes await this.ctx.storage.put('meter_state', this.meter.toState()); }}5. Consider Overage Pricing
Section titled “5. Consider Overage Pricing”For SaaS products, enable overage instead of hard limits:
budgetCheck: limit: 1000 window: month overage: enabled: true unit_price_cents: 100 # $1.00 per unit overBenefits:
- Customers never blocked (better UX)
- Additional revenue stream
- Flexible for usage spikes
6. Monitor Warning Triggers
Section titled “6. Monitor Warning Triggers”Log or alert when warnings are triggered:
const result = meter.checkBudget(customerId, Date.now(), amount);
if (result.warning === 'approaching_limit') { // Send proactive notification await sendEmail(customerId, 'quota_warning', { remaining: result.remaining, limit: config.budgetCheck.limit, });
// Log for analytics console.log({ event: 'quota_warning', customerId, remaining: result.remaining, });}7. Validate Configs at Boot
Section titled “7. Validate Configs at Boot”Parse and validate meter configs when the service starts:
import { parseMeterConfig, ConfigValidationError } from '@z0-app/sdk';
const meterConfigs = { api_calls: fs.readFileSync('meters/api_calls.yaml', 'utf-8'), storage: fs.readFileSync('meters/storage.yaml', 'utf-8'), bandwidth: fs.readFileSync('meters/bandwidth.yaml', 'utf-8'),};
const meters = new Map();
for (const [name, yaml] of Object.entries(meterConfigs)) { try { const config = parseMeterConfig(yaml); meters.set(name, new MeterEngine(name, config)); } catch (err) { if (err instanceof ConfigValidationError) { console.error(`Invalid meter config: ${name}`); err.issues.forEach(issue => console.error(` - ${issue}`)); process.exit(1); } throw err; }}Summary
Section titled “Summary”| Feature | Description | Example |
|---|---|---|
| Usage Tracking | incrementUsage() records usage atomically in all windows | meter.incrementUsage(id, now, 5) |
| Budget Enforcement | checkBudget() returns allowed/rejected with retry time | if (result.allowed) { ... } |
| Time Windows | hour, day, month - automatic rollover | windows: [hour, day, month] |
| Aggregation | count (events), sum (values) | aggregation: sum |
| Warning Threshold | Proactive alerts before hitting limit | warningThreshold: 800 |
| Overage Pricing | Allow usage beyond limit with cost | overage: { enabled: true, unit_price_cents: 50 } |
| ThresholdMonitor | Debounced threshold crossing detection | Internal, automatic |
MeterEngine provides comprehensive usage tracking and budget enforcement with flexible configuration via YAML. Combine time windows, aggregations, warning thresholds, and overage pricing to build sophisticated metering systems for SaaS products.