Usage tracking, metering infrastructure, aggregation patterns, and consumption-based billing support for SaaS applications
Tracks usage events for consumption-based billing in SaaS apps. Use when implementing API call metering, compute/storage tracking, or quota enforcement. Supports real-time, batch, and streaming ingestion patterns.
/plugin marketplace add melodic-software/claude-code-plugins/plugin install saas-patterns@melodic-softwareThis skill is limited to using the following tools:
references/aggregation-strategies.mdreferences/event-ingestion.mdGuidance for implementing usage tracking and metering infrastructure in SaaS applications.
Before implementing usage metering:
docs-management skill for billing and metering patternsUsage Categories:
┌─────────────────────────────────────────────────────────────┐
│ Category │ Examples │ Billing Unit │
├─────────────────┼────────────────────────────┼──────────────┤
│ API Calls │ Requests, queries, ops │ Per 1K calls │
│ Compute │ CPU seconds, GPU hours │ Per hour │
│ Storage │ GB stored, objects │ Per GB/month │
│ Data Transfer │ Egress, ingress bytes │ Per GB │
│ Seats/Users │ Active users, MAU │ Per user │
│ Features │ Reports generated, exports │ Per action │
│ Resources │ Projects, environments │ Per resource │
└─────────────────┴────────────────────────────┴──────────────┘
Pattern Selection:
┌──────────────────────────────────────────────────────────────┐
│ Pattern │ Use When │ Trade-offs │
├──────────────────┼───────────────────────────┼──────────────┤
│ Real-time │ Need instant visibility │ Higher cost │
│ Near-real-time │ 5-15 min latency OK │ Balanced │
│ Batch │ Daily/hourly aggregation │ Lower cost │
│ Sampling │ High volume, estimates OK │ Approximate │
│ Hybrid │ Mix of above │ Complex │
└──────────────────┴───────────────────────────┴──────────────┘
// Core usage event structure
public sealed record UsageEvent
{
public required Guid EventId { get; init; } = Guid.NewGuid();
public required Guid TenantId { get; init; }
public required string MetricName { get; init; } // e.g., "api.requests"
public required decimal Quantity { get; init; }
public required string Unit { get; init; } // e.g., "count", "bytes"
public required DateTimeOffset Timestamp { get; init; }
public required string Source { get; init; } // e.g., "api-gateway"
public string? UserId { get; init; }
public string? ResourceId { get; init; }
public Dictionary<string, string> Dimensions { get; init; } = [];
public string? IdempotencyKey { get; init; }
}
// Aggregated usage for billing periods
public sealed record UsageAggregate
{
public required Guid TenantId { get; init; }
public required string MetricName { get; init; }
public required DateOnly PeriodStart { get; init; }
public required DateOnly PeriodEnd { get; init; }
public required decimal TotalQuantity { get; init; }
public required string Unit { get; init; }
public required int EventCount { get; init; }
public DateTimeOffset LastUpdated { get; init; }
}
public interface IUsageMeteringService
{
// Record single usage event
Task RecordUsageAsync(
Guid tenantId,
string metricName,
decimal quantity,
string unit,
Dictionary<string, string>? dimensions = null,
string? idempotencyKey = null,
CancellationToken ct = default);
// Record batch of events
Task RecordBatchAsync(
IReadOnlyList<UsageEvent> events,
CancellationToken ct = default);
// Get current period usage
Task<UsageAggregate> GetCurrentUsageAsync(
Guid tenantId,
string metricName,
CancellationToken ct = default);
// Get historical usage
Task<IReadOnlyList<UsageAggregate>> GetUsageHistoryAsync(
Guid tenantId,
string metricName,
DateOnly startDate,
DateOnly endDate,
CancellationToken ct = default);
}
// Buffered writer for high-throughput scenarios
public sealed class BufferedUsageWriter : IAsyncDisposable
{
private readonly Channel<UsageEvent> _channel;
private readonly IUsageEventStore _store;
private readonly Task _processorTask;
public BufferedUsageWriter(
IUsageEventStore store,
int batchSize = 100,
TimeSpan? flushInterval = null)
{
_store = store;
_channel = Channel.CreateBounded<UsageEvent>(
new BoundedChannelOptions(10_000)
{
FullMode = BoundedChannelFullMode.Wait
});
flushInterval ??= TimeSpan.FromSeconds(5);
_processorTask = ProcessEventsAsync(batchSize, flushInterval.Value);
}
public ValueTask RecordAsync(UsageEvent evt, CancellationToken ct = default)
=> _channel.Writer.WriteAsync(evt, ct);
private async Task ProcessEventsAsync(int batchSize, TimeSpan flushInterval)
{
var batch = new List<UsageEvent>(batchSize);
var timer = new PeriodicTimer(flushInterval);
while (await _channel.Reader.WaitToReadAsync())
{
// Drain available events up to batch size
while (batch.Count < batchSize &&
_channel.Reader.TryRead(out var evt))
{
batch.Add(evt);
}
if (batch.Count >= batchSize || await ShouldFlushAsync(timer))
{
await _store.WriteBatchAsync(batch);
batch.Clear();
}
}
// Flush remaining on shutdown
if (batch.Count > 0)
await _store.WriteBatchAsync(batch);
}
}
High-Volume Metering:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ API Gateway │────▶│ Event Hub/ │────▶│ Stream │
│ (emit) │ │ Kafka │ │ Processor │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
┌──────────────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Time-Series │ │ Aggregates │ │ Alerts │
│ Store │ │ (Redis/SQL) │ │ Engine │
└─────────────┘ └─────────────┘ └─────────────┘
Low-Volume Metering:
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Application │────▶│ Usage Table │────▶│ Background │
│ (record) │ │ (append) │ │ Aggregator │
└─────────────┘ └─────────────┘ └──────┬──────┘
│
▼
┌─────────────┐
│ Aggregates │
│ Table │
└─────────────┘
public sealed class QuotaEnforcementService(
IUsageMeteringService metering,
IEntitlementService entitlements,
ILogger<QuotaEnforcementService> logger)
{
public async Task<QuotaCheckResult> CheckQuotaAsync(
Guid tenantId,
string metricName,
decimal requestedQuantity,
CancellationToken ct = default)
{
// Get current usage
var currentUsage = await metering.GetCurrentUsageAsync(
tenantId, metricName, ct);
// Get entitlement limit
var limit = await entitlements.GetLimitAsync(
tenantId, metricName, ct);
if (limit is null)
{
return QuotaCheckResult.Unlimited();
}
var projectedUsage = currentUsage.TotalQuantity + requestedQuantity;
if (projectedUsage > limit.HardLimit)
{
logger.LogWarning(
"Quota exceeded for tenant {TenantId}, metric {Metric}",
tenantId, metricName);
return QuotaCheckResult.Denied(
current: currentUsage.TotalQuantity,
limit: limit.HardLimit,
requested: requestedQuantity);
}
if (projectedUsage > limit.SoftLimit)
{
return QuotaCheckResult.Warning(
current: currentUsage.TotalQuantity,
softLimit: limit.SoftLimit,
hardLimit: limit.HardLimit);
}
return QuotaCheckResult.Allowed(
remaining: limit.HardLimit - projectedUsage);
}
}
Comparison:
┌─────────────────────────────────────────────────────────────┐
│ Aspect │ Rate Limiting │ Quota/Metering │
├─────────────────┼─────────────────────┼────────────────────┤
│ Time Window │ Seconds/minutes │ Hours/days/months │
│ Purpose │ Protect system │ Enforce billing │
│ Enforcement │ Hard block │ Soft/hard limits │
│ Response │ 429 + retry-after │ 402/403 + upgrade │
│ Tracking │ In-memory/Redis │ Durable storage │
│ Precision │ Approximate OK │ Exact required │
└─────────────────┴─────────────────────┴────────────────────┘
Use BOTH:
- Rate limiting: System protection (per-second/minute)
- Quota: Business limits (per-month billing cycles)
Load for detailed implementation:
references/event-ingestion.md - High-throughput event capture patternsreferences/aggregation-strategies.md - Time-series aggregation and rollupsFor billing integration patterns, see the billing-integration skill.
billing-integration - Payment processing and invoicingentitlements-management - Feature gating and limitssubscription-models - Pricing tier definitionsLast Updated: 2025-12-29
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.
Applies Anthropic's official brand colors and typography to any sort of artifact that may benefit from having Anthropic's look-and-feel. Use it when brand colors or style guidelines, visual formatting, or company design standards apply.
Creating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems. Create original algorithmic art rather than copying existing artists' work to avoid copyright violations.