Budget Alerts and Anomaly Detection for Geospatial Spend

A pre-merge cost estimate tells you what a change should cost. It cannot tell you that a crawler discovered your tile endpoint on Saturday, that a reprocessing job has been retrying a corrupt scene for nine hours, or that a lifecycle rule stopped matching after a prefix rename and six months of imagery is still sitting in the most expensive storage class. Those are runtime cost events, and they need runtime detection. This guide is the operational half of Cost Estimation Frameworks, within Spatial IaC Architecture and Fundamentals, and it covers the alerting that catches what the estimate structurally cannot.

Symptom identification and triage

Geospatial cost incidents have a characteristic shape that distinguishes them from ordinary cloud overspend: the driver is almost always volume rather than rate, and the volume is usually egress or compute-seconds rather than storage. Four patterns account for most of them.

Tile egress spike. A sudden climb in data transfer with flat request counts at the origin, meaning the CDN is serving more but the platform is not working harder. Usually a scraper, an embedded map on a newly popular page, or a client that disabled its cache. Distinguishable from legitimate growth by its shape: real growth is gradual and diurnal, a scraper is a step function that ignores time of day.

Retry storms in a raster pipeline. Compute-seconds climb with no increase in successfully processed scenes. One malformed input that fails after twenty minutes and retries indefinitely can consume more compute in a weekend than a month of normal ingestion. The signature is a rising ratio of invocations to completions.

Storage-class drift. Storage cost rises steadily while object count is flat. Something stopped transitioning — most often a lifecycle rule whose prefix no longer matches after a naming change, which is silent because a rule that matches nothing is not an error.

Cross-zone or NAT traffic from a topology change. Data-transfer charges appear on an internal path that used to be free, typically after a renderer was rescheduled into a different availability zone from its database, or after a gateway endpoint was removed and object reads began routing through NAT.

Four cost-incident signatures in a spatial platform Each incident type is identified by a pair of metrics moving differently. A tile egress spike shows data transfer rising while origin request count stays flat, because the content delivery network absorbs the requests. A retry storm shows invocation count rising while successful completions stay flat. Storage class drift shows storage cost rising while object count stays flat, meaning objects stopped transitioning rather than accumulating. An internal transfer charge appears with no change in any workload metric at all, and correlates instead with a topology change such as a renderer rescheduled away from its database. Tile egress spike transfer rising · origin requests flat step function, ignores the diurnal cycle scraper, embed, or a client cache turned off Retry storm invocations rising · completions flat one malformed scene, retried indefinitely a weekend can outspend a month Storage class drift storage cost rising · object count flat a lifecycle prefix that no longer matches silent: a rule matching nothing is not an error Internal transfer workload metrics unchanged correlates with a topology change cross-zone hop, or a lost gateway endpoint

Prerequisites and environment assumptions

Terraform 1.6 or later with hashicorp/aws pinned at ~> 5.60. A tag contract already enforced, because every alert here is scoped by tag and an untagged resource is invisible to all of them — the labels described in FinOps Tagging Strategies for Geospatial Resources are a hard dependency, not a nice-to-have. Cost allocation tags activated in the billing account, and at least one full billing cycle of history so anomaly detection has a baseline to compare against.

Decide before you build who receives these alerts. A cost alert routed to a finance mailbox arrives days after the engineer who could stop the bleeding has gone home. Route the anomaly alerts to the same on-call channel that receives operational alarms, and reserve the monthly budget summaries for the finance recipients.

Step-by-step implementation

  1. Set a budget per workload, not per account. An account-level budget on a platform running ingestion, serving and analytics tells you that something is expensive, which you already knew. Budgets scoped by the WorkloadType tag tell you which thing, and that is the difference between an alert and a diagnosis.

  2. Alert on forecast, not only on actual. An actual-spend threshold at 80 per cent fires on the twenty-fourth day of a month for a workload that will overshoot by 40 per cent. A forecast threshold fires on the fifth, while the month is still recoverable. Configure both: forecast for early warning, actual for the hard limit.

  3. Enable anomaly detection on the dimensions that move. Monitors on data transfer and on compute for the spatial services catch the volume-driven incidents that a fixed threshold misses, because they compare against the workload’s own learned pattern rather than a number someone chose a quarter ago.

  4. Add a ratio alarm for retry storms. Cost tooling will not catch this cheaply, because the spend rises smoothly. An operational alarm on invocations divided by successful completions catches it in minutes rather than at the next billing refresh, which is often a day later.

  5. Close the loop with an automated response for the worst case. A budget action that attaches a restrictive policy when a threshold is breached is appropriate for non-production environments. In production, page instead — an automated throttle on a live tile platform converts a cost incident into an availability incident.

terraform {
  required_version = ">= 1.6.0"
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.60"
    }
  }
}

variable "oncall_topic_arn" { type = string }
variable "finance_email" { type = string }

resource "aws_budgets_budget" "tile_serving" {
  name         = "tile-serving-monthly"
  budget_type  = "COST"
  limit_amount = "4000"
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  # Scoped by tag: an account-level budget says something is expensive,
  # a workload-scoped one says which thing.
  cost_filter {
    name   = "TagKeyValue"
    values = ["user:WorkloadType$tile-serving"]
  }

  # Forecast first — this fires on day five, while the month is recoverable.
  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 100
    threshold_type             = "PERCENTAGE"
    notification_type          = "FORECASTED"
    subscriber_sns_topic_arns  = [var.oncall_topic_arn]
  }

  # Actual as the hard limit, to finance as well as on-call.
  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 90
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_sns_topic_arns  = [var.oncall_topic_arn]
    subscriber_email_addresses = [var.finance_email]
  }
}

resource "aws_ce_anomaly_monitor" "spatial_transfer" {
  name              = "spatial-data-transfer"
  monitor_type      = "DIMENSIONAL"
  monitor_dimension = "SERVICE"
}

resource "aws_ce_anomaly_subscription" "spatial_transfer" {
  name      = "spatial-transfer-anomalies"
  frequency = "IMMEDIATE"
  monitor_arn_list = [aws_ce_anomaly_monitor.spatial_transfer.arn]

  subscriber {
    type    = "SNS"
    address = var.oncall_topic_arn
  }

  # An absolute floor stops routine noise; the detector still compares against
  # the workload's own learned pattern rather than a number chosen last quarter.
  threshold_expression {
    dimension {
      key           = "ANOMALY_TOTAL_IMPACT_ABSOLUTE"
      match_options = ["GREATER_THAN_OR_EQUAL"]
      values        = ["150"]
    }
  }
}

# Retry storms rise smoothly in cost and are invisible to a budget until the
# next billing refresh. The ratio catches them in minutes.
resource "aws_cloudwatch_metric_alarm" "raster_retry_ratio" {
  alarm_name          = "raster-pipeline-retry-ratio"
  comparison_operator = "GreaterThanThreshold"
  evaluation_periods  = 2
  threshold           = 3

  metric_query {
    id          = "ratio"
    expression  = "invocations / IF(completions > 0, completions, 1)"
    label       = "invocations per completion"
    return_data = true
  }
  metric_query {
    id = "invocations"
    metric {
      namespace   = "SpatialPlatform/Raster"
      metric_name = "JobInvocations"
      period      = 300
      stat        = "Sum"
    }
  }
  metric_query {
    id = "completions"
    metric {
      namespace   = "SpatialPlatform/Raster"
      metric_name = "JobCompletions"
      period      = 300
      stat        = "Sum"
    }
  }

  alarm_actions = [var.oncall_topic_arn]
}
Detection layers ordered by how quickly they can fire Three layers cover different delays. An operational ratio alarm built on the platform's own metrics fires within minutes and is the only layer fast enough to catch a retry storm before it consumes a weekend of compute. An anomaly monitor compares against the workload's learned pattern and fires within hours, bounded by the billing data refresh. A forecast budget alert fires within days and is early enough that the month is still recoverable. An actual-spend threshold fires last and functions as a hard limit rather than a warning, which is why it must not be the only alert configured. minutes hours days month end Ratio alarm catches retry storms Anomaly monitor learned pattern Forecast budget month still recoverable Actual spend a limit, not a warning

Verification

Prove each alert can fire before you rely on it. For the budget notifications, set a temporary limit below current spend in a non-production account and confirm the notification arrives at the intended destination — this validates the tag filter, which is where these silently fail, since a filter matching no resources produces a budget that is permanently at zero per cent and looks healthy.

For the ratio alarm, publish synthetic values to the two metrics and confirm the expression evaluates and the alarm transitions. For anomaly detection, confirm the monitor has ingested at least a full cycle of history and shows a baseline; a monitor created yesterday will not fire meaningfully today, and believing otherwise leaves a gap nobody is watching. The general technique is the alarm-firing test described in Observability for Spatial Infrastructure.

Proving each alert can fire, by the method that suits it A budget notification is proven by setting a temporary limit below current spend in a non-production account and confirming the notification arrives, which simultaneously validates the tag filter — the part that silently fails, because a filter matching nothing produces a budget permanently at zero per cent that looks healthy. A ratio alarm is proven by publishing synthetic values to its two component metrics and confirming the expression evaluates and the alarm transitions. An anomaly monitor cannot be triggered on demand at all, so it is verified differently: by confirming it has ingested at least a full cycle of history and reports a baseline, since a monitor created yesterday will not fire meaningfully today. Budget notification temporary low limit in a non-production account also validates the tag filter where these silently fail Ratio alarm publish synthetic values to both component metrics confirm the expression evaluates and the state transitions Anomaly monitor cannot be triggered at all so confirm it has a baseline from a full cycle of history one created yesterday will not fire

Preventing recurrence

  • Make the tag contract a merge gate. Every alert here is tag-scoped, so an untagged resource is an unmonitored resource. Enforce the tags in the policy stage rather than auditing for them later.
  • Attach a cost owner to each budget. A budget alert with no named owner is a notification, not an action.
  • Re-baseline after deliberate growth. A launch that legitimately triples traffic should be followed by an explicit threshold update, or every subsequent week produces a false alarm and the alerts get muted.
  • Review the four signatures quarterly. Each of the incident patterns above corresponds to a specific control — cache policy, retry limits, lifecycle prefixes, endpoint routing — and reviewing them is cheaper than detecting their failure.

Frequently Asked Questions

Why not just set an account-level budget and be done?

Because it tells you that something is expensive without telling you what, and on a spatial platform the plausible causes — egress, raster compute, database size, storage class — have entirely different remedies. A workload-scoped budget is a diagnosis; an account-scoped one is a prompt to start investigating.

How quickly will an anomaly alert actually arrive?

Bounded by how often billing data refreshes, which is typically several hours, so anomaly detection is not a real-time control. That delay is precisely why the operational ratio alarm exists: it runs on your own metrics and can fire in minutes.

Should a budget breach automatically throttle the platform?

In non-production, yes — an automated restriction is the correct response to a runaway development environment. In production, no. Converting a cost incident into an availability incident is almost never the right trade, and the decision to degrade service should be made by a human with the context.

What is the fastest way to confirm a tile egress spike is a scraper?

Compare the request pattern against the diurnal cycle and check the cache hit ratio by user agent and referrer at the edge. Human map traffic follows working hours and has a high hit ratio; a crawler walks the tile grid at a constant rate around the clock and misses the cache constantly because it requests coordinates nobody else does.