Skip to content

MeterEngine

Usage tracking and budget enforcement system for metering entity activity.

Prerequisites: projection-engine.md


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: entity
factTypes: [api_call]
unit: requests
aggregation: count
windows: [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 usage
meter.incrementUsage('customer_123', Date.now(), 5);
// Check budget before allowing operation
const 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)}`);
}

MeterEngine uses YAML configuration parsed via parseMeterConfig().

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
}
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
}
}

Use incrementUsage() to atomically record usage across all configured windows:

const meter = new MeterEngine('api_calls', config);
// Record usage for an entity
meter.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)

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 identifier
  • window - Time window to query (‘hour’ | ‘day’ | ‘month’)
  • timestamp - Optional reference timestamp (defaults to Date.now())

Returns: Current usage count for the window, or 0 if no usage recorded.

const config = parseMeterConfig(`
source: api
factTypes: [request_completed]
unit: requests
aggregation: count
windows: [hour, day]
`);
const meter = new MeterEngine('api_calls', config);
// Record requests
meter.incrementUsage('customer_123', Date.now(), 1);
meter.incrementUsage('customer_123', Date.now(), 1);
meter.incrementUsage('customer_123', Date.now(), 1);
// Query usage
const 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"

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)}`);
}

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}`
);
}
}

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"
}

Enforce a hard limit without warnings or overage:

source: api
factTypes: [request_completed]
unit: requests
aggregation: count
windows: [day]
budgetCheck:
limit: 1000
window: day
// After 1000 requests, checkBudget returns allowed: false

Alert when approaching limit (integrates with ThresholdMonitor):

budgetCheck:
limit: 1000
window: day
warningThreshold: 800 # Warn when 800+ used
const 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 ThresholdMonitor internally for debounced alerts
  • Only triggers warning once when crossing threshold
  • Resets after budget window rolls over

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 requests
const 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
// }
// }

MeterEngine tracks usage across configured time windows, automatically resetting when windows roll over.

WindowFormatReset Behavior
hourYYYY-MM-DDTHHResets at top of next hour (UTC)
dayYYYY-MM-DDResets at midnight UTC
monthYYYY-MMResets on 1st of next month (UTC)

All timestamps use UTC timezone.

const config = parseMeterConfig(`
source: api
factTypes: [request]
unit: requests
aggregation: count
windows: [day]
budgetCheck:
limit: 100
window: day
`);
const meter = new MeterEngine('daily_limit', config);
// March 12, 2026 - 95 requests
const march12 = Date.UTC(2026, 2, 12, 22, 0, 0);
meter.incrementUsage('customer_123', march12, 95);
// Check usage on March 12
const usage12 = meter.getUsage('customer_123', 'day', march12);
console.log(usage12); // 95
// March 13, 2026 - new day, new window
const 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.window
const result = meter.checkBudget('customer_123', march13, 50);
console.log(result.allowed); // true (new window)

Track usage across multiple windows simultaneously:

source: api
factTypes: [request]
unit: requests
aggregation: count
windows: [hour, day, month] # Track all three
budgetCheck:
limit: 10000
window: month # Enforce monthly limit
const meter = new MeterEngine('multi_window', config);
meter.incrementUsage('customer_123', Date.now(), 50);
// Query each window
const 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`);

Count the number of times an event occurs:

source: api
factTypes: [request_completed]
unit: requests
aggregation: count
windows: [day]
budgetCheck:
limit: 1000
window: day
// Each increment adds 1
meter.incrementUsage('customer_123', Date.now(), 1);
meter.incrementUsage('customer_123', Date.now(), 1);
const usage = meter.getUsage('customer_123', 'day');
console.log(usage); // 2

Sum numeric values (bytes, minutes, tokens, etc.):

source: storage
factTypes: [file_uploaded, file_deleted]
unit: bytes
aggregation: sum
windows: [day, month]
budgetCheck:
limit: 10000000 # 10 MB daily limit
window: day
// Track file uploads by size
meter.incrementUsage('customer_123', Date.now(), 2500000); // 2.5 MB
meter.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 limit
const result = meter.checkBudget('customer_123', Date.now(), 7000000); // 7 MB
console.log(result.allowed); // false (4 + 7 > 10)

MeterEngine uses ThresholdMonitor internally for warning threshold detection with automatic debounce.

When warningThreshold is configured:

  1. Before each budget check, MeterEngine calculates remaining budget
  2. ThresholdMonitor.check() compares old remaining vs. new remaining
  3. If crossing threshold, sets warning: 'approaching_limit'
  4. Debounce prevents repeated warnings until budget resets or usage decreases
const config = parseMeterConfig(`
source: api
factTypes: [request]
unit: requests
aggregation: count
windows: [day]
budgetCheck:
limit: 1000
window: day
warningThreshold: 800
`);
const meter = new MeterEngine('api_meter', config);
const entityId = 'customer_123';
// Usage builds up
meter.incrementUsage(entityId, Date.now(), 700);
// Check budget - no warning yet
let result = meter.checkBudget(entityId, Date.now(), 50);
console.log(result.warning); // undefined (750 < 800)
// Increment to cross threshold
meter.incrementUsage(entityId, Date.now(), 100);
// Check budget - warning triggered
result = meter.checkBudget(entityId, Date.now(), 10);
console.log(result.warning); // 'approaching_limit'
// Send alert
if (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

Enforce per-customer API request limits:

source: api_gateway
factTypes: [request_completed]
unit: requests
aggregation: count
windows: [hour, day]
budgetCheck:
limit: 10000
window: day
warningThreshold: 8000
const config = parseMeterConfig(yaml);
const meter = new MeterEngine('api_rate_limit', config);
// Middleware function
async 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);
}

Track storage usage with soft limits and overage billing:

source: storage
factTypes: [file_uploaded]
unit: bytes
aggregation: sum
windows: [month]
budgetCheck:
limit: 10737418240 # 10 GB
window: month
warningThreshold: 9663676416 # 9 GB
overage:
enabled: true
unit_price_cents: 10 # $0.10 per GB over limit
const 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);
}

Track compute usage with budget enforcement:

source: compute
factTypes: [job_completed]
unit: minutes
aggregation: sum
windows: [day, month]
budgetCheck:
limit: 10000
window: month
warningThreshold: 8000
const 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 };
}

Use multiple meters per entity for different resource types:

// API rate limiter
const apiConfig = parseMeterConfig(`
source: api
factTypes: [request]
unit: requests
aggregation: count
windows: [hour, day]
budgetCheck:
limit: 1000
window: hour
`);
const apiMeter = new MeterEngine('api_calls', apiConfig);
// Bandwidth tracker
const bandwidthConfig = parseMeterConfig(`
source: network
factTypes: [data_transfer]
unit: bytes
aggregation: sum
windows: [day, month]
budgetCheck:
limit: 107374182400 # 100 GB/month
window: month
`);
const bandwidthMeter = new MeterEngine('bandwidth', bandwidthConfig);
// Storage quota
const storageConfig = parseMeterConfig(`
source: storage
factTypes: [file_operation]
unit: bytes
aggregation: sum
windows: [month]
budgetCheck:
limit: 10737418240 # 10 GB
window: month
`);
const storageMeter = new MeterEngine('storage', storageConfig);
// Check all meters before operation
async 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 };
}

Always check budget before performing the operation:

// ✅ GOOD - Check before operation
const 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);

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

Match window to use case:

  • hour - Real-time rate limiting (burst protection)
  • day - Daily quotas (API calls, compute minutes)
  • month - Billing cycles (storage, bandwidth)

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());
}
}

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 over

Benefits:

  • Customers never blocked (better UX)
  • Additional revenue stream
  • Flexible for usage spikes

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,
});
}

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;
}
}

FeatureDescriptionExample
Usage TrackingincrementUsage() records usage atomically in all windowsmeter.incrementUsage(id, now, 5)
Budget EnforcementcheckBudget() returns allowed/rejected with retry timeif (result.allowed) { ... }
Time Windowshour, day, month - automatic rolloverwindows: [hour, day, month]
Aggregationcount (events), sum (values)aggregation: sum
Warning ThresholdProactive alerts before hitting limitwarningThreshold: 800
Overage PricingAllow usage beyond limit with costoverage: { enabled: true, unit_price_cents: 50 }
ThresholdMonitorDebounced threshold crossing detectionInternal, 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.