From e9d0bdf19b627935a5672d507e39ec2f84a5d044 Mon Sep 17 00:00:00 2001 From: Daniel Salazar Date: Wed, 4 Feb 2026 13:08:18 -0800 Subject: [PATCH] feat: add alerting for usage abuse (#2413) --- .../MeteringService/MeteringService.ts | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/backend/src/services/MeteringService/MeteringService.ts b/src/backend/src/services/MeteringService/MeteringService.ts index 8f4e7db43..3c53ce487 100644 --- a/src/backend/src/services/MeteringService/MeteringService.ts +++ b/src/backend/src/services/MeteringService/MeteringService.ts @@ -8,6 +8,7 @@ import { DEFAULT_FREE_SUBSCRIPTION, DEFAULT_TEMP_SUBSCRIPTION, GLOBAL_APP_KEY, M import { COST_MAPS } from './costMaps/index.js'; import { SUB_POLICIES } from './subPolicies/index.js'; import { AppTotals, MeteringServiceDeps, UsageAddons, UsageByType, UsageRecord } from './types.js'; +import { toMicroCents } from './utils.js'; /** * Handles usage metering and supports stubbs for billing methods for current scoped actor */ @@ -15,6 +16,7 @@ export class MeteringService { static GLOBAL_SHARD_COUNT = 1000; // number of global usage shards to spread writes across static APP_SHARD_COUNT = 1000; // number of app usage shards to spread writes across + static MAX_GLOBAL_USAGE_PER_MINUTE = toMicroCents(.2); // 20 cents per minute max global usage to help detect abuse #kvStore: DynamoKVStore; #superUserService: SUService; #alarmService: AlarmService; @@ -24,6 +26,9 @@ export class MeteringService { this.#kvStore = kvStore; this.#alarmService = alarmService; this.#eventService = eventService; + setInterval(() => { + this.#checkRateOfChange(); + }, 1000 * 60 * 5); // check every 5 minutes } utilRecordUsageObject>(trackedUsageObject: T, actor: Actor, modelPrefix: string, costsOverrides?: Partial>) { @@ -641,4 +646,37 @@ export class MeteringService { }); }); } + + async #checkRateOfChange () { + const globalUsage = await this.getGlobalUsage(); + const now = Date.now(); + const lastChange = await this.#superUserService.sudo(async () => { + return this.#kvStore.get({ key: `${METRICS_PREFIX}:lastGlobalUsageCheck` }) as Promise<{ total: number, timestamp: number } | null>; + }); + + const currTotal = globalUsage.total; + + if ( lastChange ) { + const timeDelta = now - lastChange.timestamp; + const usageDelta = currTotal - lastChange.total; + const usagePerMinute = (usageDelta / (timeDelta / 60000)); + + if ( usagePerMinute > MeteringService.MAX_GLOBAL_USAGE_PER_MINUTE ) { + this.#alarmService.create('metering:excessiveGlobalUsageRate', `Global usage rate is excessive: ${usagePerMinute} micro-cents per minute`, { + usagePerMinute, + maxAllowedPerMinute: MeteringService.MAX_GLOBAL_USAGE_PER_MINUTE, + }); + } + } + await this.#superUserService.sudo(async () => { + await this.#kvStore.set({ + key: `${METRICS_PREFIX}:lastGlobalUsageCheck`, + value: { + total: currTotal, + timestamp: now, + }, + }); + }); + + } }