DEV Community

serkan
serkan

Posted on

Building Simple Anomaly Detection for API Cost Tracking (No ML Required)

The problem

If you're tracking costs for any kind of usage-based API (LLM calls, cloud compute, etc.), you eventually want to know: "is today's spend unusual?"

You don't need machine learning for this. A simple statistical comparison against a rolling average works well enough for most cases.

The approach

Calculate the average daily cost over the past N days, then compare today's cost against that average. If it's significantly higher (I used 2x as the threshold), flag it.

function detectAnomaly(dailyCosts) {
  // dailyCosts is an array of { date, cost }, sorted chronologically
  if (dailyCosts.length < 2) return null

  const previousDays = dailyCosts.slice(0, -1)
  const avgCost = previousDays.reduce((sum, d) => sum + d.cost, 0) / previousDays.length

  const today = dailyCosts[dailyCosts.length - 1]
  const isAnomaly = avgCost > 0 && today.cost > avgCost * 2

  return isAnomaly ? { today: today.cost, average: avgCost } : null
}
Enter fullscreen mode Exit fullscreen mode

Why 2x and not something more sophisticated

I considered standard deviation-based approaches (z-scores), but for small datasets (a week or two of daily costs), a simple multiplier is more predictable and easier to explain to users. "Your spend is 2x your average" is immediately understandable. "Your spend is 2.3 standard deviations above the mean" requires more context.

If you have enough historical data (weeks or months), z-scores become more reliable since they account for natural variance in your data.

A related problem: detecting duplicate work

While building this, I also added detection for repeated identical requests — useful for catching when an app is re-sending the same prompt to an LLM API without caching.

function findDuplicates(requests) {
  const counts = {}
  for (const req of requests) {
    const key = req.prompt
    if (!counts[key]) counts[key] = { count: 0, totalCost: 0 }
    counts[key].count++
    counts[key].totalCost += req.cost
  }

  return Object.entries(counts)
    .filter(([, data]) => data.count > 1)
    .map(([prompt, data]) => ({
      prompt,
      count: data.count,
      potentialSavings: data.totalCost - (data.totalCost / data.count),
    }))
}
Enter fullscreen mode Exit fullscreen mode

The savings estimate assumes you'd cache after the first call, so you only pay once instead of N times.

Where this lives

Built this into LLMWatch, a tool I'm building for tracking LLM API costs. Both checks run client-side on data the dashboard already has, so there's no extra infrastructure needed — just array operations on data you're already fetching.

Top comments (0)