Aws

AWS Cost Management and Budgets

Control AWS spending with Cost Explorer analysis, budget alerts, cost allocation tags, and programmatic monitoring with Node.js

AWS Cost Management and Budgets

AWS billing has a way of surprising you. You spin up a few services for a proof of concept, forget about a NAT Gateway running in a VPC you stopped using, and next month you are staring at a bill that is three times what you expected. AWS Cost Management is the set of tools that prevents that from happening — or at least catches it before it spirals.

This article covers the full stack of AWS cost control: from the console-based tools like Cost Explorer and Budgets, through cost allocation strategies, all the way to building a programmatic Node.js monitoring dashboard that alerts you in Slack when spending exceeds your thresholds.

Prerequisites

  • An AWS account with billing access (root or IAM user with ce:*, budgets:*, and cloudwatch:* permissions)
  • Node.js v16 or later
  • AWS CLI configured with credentials (aws configure)
  • Basic familiarity with AWS services (EC2, S3, Lambda)
  • A Slack workspace with an incoming webhook URL (for the alerting example)

AWS Cost Explorer and Usage Analysis

Cost Explorer is the first place you should go when you need to understand your AWS bill. It gives you a visual breakdown of spending over time, filterable by service, account, region, and tag.

Enabling Cost Explorer

Cost Explorer is not enabled by default. You need to turn it on from the Billing console, and it takes about 24 hours before data starts populating. Once enabled, it retains 13 months of historical data.

Key Views to Set Up

There are three views I configure on every AWS account I manage:

  1. Monthly cost by service — Shows which services are driving your bill. Sort by cost descending.
  2. Daily cost trend — Catches spending spikes early. If your daily cost jumps 40% on a Tuesday, you want to know before the month closes.
  3. Cost by linked account — If you are running AWS Organizations, this tells you which team or project is responsible for what.

Reading Cost Explorer Programmatically

The AWS SDK exposes Cost Explorer through the @aws-sdk/client-cost-explorer package. Here is how you pull the last 30 days of cost data grouped by service:

var AWS = require("@aws-sdk/client-cost-explorer");
var client = new AWS.CostExplorerClient({ region: "us-east-1" });

function getLast30DaysCostByService(callback) {
  var now = new Date();
  var thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);

  var params = {
    TimePeriod: {
      Start: thirtyDaysAgo.toISOString().split("T")[0],
      End: now.toISOString().split("T")[0]
    },
    Granularity: "MONTHLY",
    Metrics: ["UnblendedCost"],
    GroupBy: [
      {
        Type: "DIMENSION",
        Key: "SERVICE"
      }
    ]
  };

  var command = new AWS.GetCostAndUsageCommand(params);
  client.send(command, function(err, data) {
    if (err) {
      return callback(err);
    }
    var results = [];
    data.ResultsByTime.forEach(function(period) {
      period.Groups.forEach(function(group) {
        results.push({
          service: group.Keys[0],
          cost: parseFloat(group.Metrics.UnblendedCost.Amount).toFixed(2)
        });
      });
    });
    results.sort(function(a, b) { return b.cost - a.cost; });
    callback(null, results);
  });
}

Cost Explorer API calls are not free. Each GetCostAndUsage request costs $0.01. That is negligible for a dashboard that refreshes every few hours, but do not put it in a tight loop.

Setting Up Budgets and Alerts

AWS Budgets lets you set a monthly spending cap and get notified when you approach it. You can create budgets through the console, CLI, or SDK.

Console Setup

Navigate to Billing > Budgets > Create budget. Choose "Cost budget" for overall spending. Set a monthly amount, then configure alerts at 80%, 90%, and 100% of the budget. Add email recipients for each threshold.

Programmatic Budget Creation

var BudgetsSDK = require("@aws-sdk/client-budgets");
var budgetsClient = new BudgetsSDK.BudgetsClient({ region: "us-east-1" });

function createMonthlyBudget(accountId, budgetName, limitAmount, emails, callback) {
  var params = {
    AccountId: accountId,
    Budget: {
      BudgetName: budgetName,
      BudgetLimit: {
        Amount: String(limitAmount),
        Unit: "USD"
      },
      BudgetType: "COST",
      TimeUnit: "MONTHLY"
    },
    NotificationsWithSubscribers: [
      {
        Notification: {
          NotificationType: "ACTUAL",
          ComparisonOperator: "GREATER_THAN",
          Threshold: 80,
          ThresholdType: "PERCENTAGE"
        },
        Subscribers: emails.map(function(email) {
          return { SubscriptionType: "EMAIL", Address: email };
        })
      },
      {
        Notification: {
          NotificationType: "FORECASTED",
          ComparisonOperator: "GREATER_THAN",
          Threshold: 100,
          ThresholdType: "PERCENTAGE"
        },
        Subscribers: emails.map(function(email) {
          return { SubscriptionType: "EMAIL", Address: email };
        })
      }
    ]
  };

  var command = new BudgetsSDK.CreateBudgetCommand(params);
  budgetsClient.send(command, function(err, data) {
    if (err) return callback(err);
    callback(null, data);
  });
}

The forecasted alert at 100% is critical. It uses AWS's projection algorithm to estimate your end-of-month total based on current trajectory. If you are on track to overshoot, you get warned before it actually happens.

Cost Allocation Tags

Tags are how you attribute costs to specific projects, teams, or environments. Without them, your bill is just a list of services with no context about what they support.

Activating Cost Allocation Tags

AWS supports two types of tags for billing purposes:

  • AWS-generated tags — Things like aws:createdBy that AWS applies automatically.
  • User-defined tags — Tags you create, like Project, Environment, or Team.

You activate cost allocation tags in Billing > Cost allocation tags. User-defined tags must be activated here before they appear in Cost Explorer. It takes 24 hours after activation for tagged costs to show up.

Tagging Strategy

Every resource should have at minimum:

Project: my-api-service
Environment: production | staging | development
Team: backend | frontend | data
CostCenter: ENG-001

Enforce tagging through AWS Organizations Service Control Policies (SCPs) or use AWS Config rules to flag untagged resources. Untagged resources are invisible in cost allocation reports, which defeats the purpose.

Querying Costs by Tag

function getCostByTag(tagKey, callback) {
  var now = new Date();
  var thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);

  var params = {
    TimePeriod: {
      Start: thirtyDaysAgo.toISOString().split("T")[0],
      End: now.toISOString().split("T")[0]
    },
    Granularity: "MONTHLY",
    Metrics: ["UnblendedCost"],
    GroupBy: [
      {
        Type: "TAG",
        Key: tagKey
      }
    ]
  };

  var command = new AWS.GetCostAndUsageCommand(params);
  client.send(command, function(err, data) {
    if (err) return callback(err);
    var results = [];
    data.ResultsByTime.forEach(function(period) {
      period.Groups.forEach(function(group) {
        var tagValue = group.Keys[0].replace(tagKey + "$", "");
        results.push({
          tag: tagValue || "(untagged)",
          cost: parseFloat(group.Metrics.UnblendedCost.Amount).toFixed(2)
        });
      });
    });
    callback(null, results);
  });
}

Reserved Instances vs Savings Plans

If you are running steady-state workloads, on-demand pricing is the most expensive way to pay. There are two commitment-based discount models.

Reserved Instances (RIs)

RIs give you a discount (up to 72%) for committing to a specific instance type in a specific region for 1 or 3 years. They work well when your infrastructure is predictable and you know exactly what instance types you need.

The downside: inflexibility. If you commit to m5.xlarge in us-east-1 and later migrate to m6i.xlarge or move regions, your RI does not follow you (unless you bought a convertible RI, which has a smaller discount).

Savings Plans

Savings Plans are the newer, more flexible option. You commit to a dollar amount of compute per hour (for example, $10/hour) rather than a specific instance type. There are two kinds:

  • Compute Savings Plans — Apply across EC2, Fargate, and Lambda in any region. Most flexible, up to 66% discount.
  • EC2 Instance Savings Plans — Locked to an instance family in a region. Less flexible, up to 72% discount.

My recommendation: start with Compute Savings Plans unless you have a very stable, well-known workload that will not change instance types. The flexibility premium is worth the slightly lower discount.

Checking Savings Plan Utilization

var command = new AWS.GetSavingsPlansUtilizationCommand({
  TimePeriod: {
    Start: "2026-01-01",
    End: "2026-02-01"
  }
});

client.send(command, function(err, data) {
  if (err) return console.error(err);
  var utilization = data.Total.Utilization;
  console.log("Utilization: " + utilization.UtilizationPercentage + "%");
  console.log("Savings: $" + utilization.NetSavings);
});

If utilization is below 80%, you over-committed. Scale back your next Savings Plan purchase.

Right-Sizing Recommendations

AWS Compute Optimizer analyzes your EC2 instances, Lambda functions, and EBS volumes, then recommends cheaper alternatives based on actual utilization. If you have an m5.2xlarge running at 15% CPU, it will tell you to drop to an m5.large.

Pull these recommendations programmatically:

var ComputeOptimizer = require("@aws-sdk/client-compute-optimizer");
var optimizerClient = new ComputeOptimizer.ComputeOptimizerClient({ region: "us-east-1" });

function getEC2Recommendations(callback) {
  var command = new ComputeOptimizer.GetEC2InstanceRecommendationsCommand({});
  optimizerClient.send(command, function(err, data) {
    if (err) return callback(err);
    var recs = data.instanceRecommendations.map(function(rec) {
      return {
        instanceId: rec.instanceArn.split("/").pop(),
        currentType: rec.currentInstanceType,
        finding: rec.finding,
        recommendations: rec.recommendationOptions.map(function(opt) {
          return {
            instanceType: opt.instanceType,
            projectedUtilization: opt.projectedUtilizationMetrics,
            estimatedMonthlySavings: opt.savingsOpportunity
              ? opt.savingsOpportunity.estimatedMonthlySavings.value
              : "N/A"
          };
        })
      };
    });
    callback(null, recs);
  });
}

Compute Optimizer requires CloudWatch agent metrics for accurate recommendations. Without it, the recommendations are based solely on basic EC2 metrics, which miss memory utilization entirely.

Lambda Cost Optimization

Lambda pricing has two components: request count and duration (GB-seconds). The optimization strategies differ for each.

Memory vs Duration Tradeoff

Lambda bills per millisecond of execution time, multiplied by allocated memory. Counterintuitively, increasing memory often reduces cost because the function runs faster (Lambda allocates proportional CPU with memory). A function running at 128MB for 3 seconds may cost more than the same function at 512MB completing in 400ms.

Use AWS Lambda Power Tuning (an open-source tool) to find the optimal memory setting for each function.

Provisioned Concurrency

If your Lambda functions have consistent traffic, provisioned concurrency eliminates cold starts but adds a flat hourly charge. Only use it if cold start latency is a business problem — otherwise, you are paying for idle compute.

ARM64 Architecture

Graviton-based Lambda functions (arm64) are 20% cheaper per GB-second than x86_64 and often run faster. Unless you have native x86 dependencies, switch your runtime architecture.

S3 and Data Transfer Costs

S3 storage costs are usually modest. The surprise comes from data transfer and request charges.

Storage Classes

Class Use Case Cost (us-east-1)
Standard Frequently accessed $0.023/GB
Intelligent-Tiering Unknown access patterns $0.023/GB + monitoring fee
Standard-IA Infrequent access (30-day min) $0.0125/GB
Glacier Instant Archive with ms retrieval $0.004/GB
Glacier Deep Archive Compliance archives $0.00099/GB

Set up S3 Lifecycle rules to automatically transition objects to cheaper tiers as they age.

Data Transfer Traps

  • Cross-region replication: You pay for PUT requests and data transfer between regions.
  • NAT Gateway data processing: $0.045/GB. If your Lambda functions in a VPC are calling S3, use a VPC endpoint ($0.01/GB) instead.
  • CloudFront to origin: Data transfer from S3 to CloudFront is free. Put CloudFront in front of your S3 buckets if you are serving content publicly.

Billing Alarms with CloudWatch

CloudWatch billing metrics are a simpler alternative to AWS Budgets for basic threshold alerts. They only work in us-east-1.

var CloudWatch = require("@aws-sdk/client-cloudwatch");
var cwClient = new CloudWatch.CloudWatchClient({ region: "us-east-1" });

function createBillingAlarm(alarmName, threshold, snsTopicArn, callback) {
  var params = {
    AlarmName: alarmName,
    MetricName: "EstimatedCharges",
    Namespace: "AWS/Billing",
    Statistic: "Maximum",
    Period: 21600,
    EvaluationPeriods: 1,
    Threshold: threshold,
    ComparisonOperator: "GreaterThanThreshold",
    AlarmActions: [snsTopicArn],
    Dimensions: [
      {
        Name: "Currency",
        Value: "USD"
      }
    ]
  };

  var command = new CloudWatch.PutMetricAlarmCommand(params);
  cwClient.send(command, function(err, data) {
    if (err) return callback(err);
    console.log("Billing alarm created: " + alarmName);
    callback(null, data);
  });
}

You must enable "Receive Billing Alerts" in the Billing preferences before these metrics appear.

Cost Anomaly Detection

AWS Cost Anomaly Detection uses machine learning to identify unusual spending patterns. It learns your baseline over time and alerts you when something deviates.

Set up a cost monitor for your AWS account and attach an alert subscription (email or SNS). It catches things like:

  • A misconfigured auto-scaling group spinning up 200 instances
  • An S3 bucket suddenly receiving 10x the normal request volume
  • A forgotten Elasticsearch domain running in a region you do not use
function getAnomalies(callback) {
  var now = new Date();
  var thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);

  var params = {
    DateInterval: {
      StartDate: thirtyDaysAgo.toISOString().split("T")[0],
      EndDate: now.toISOString().split("T")[0]
    },
    MaxResults: 20
  };

  var command = new AWS.GetAnomaliesCommand(params);
  client.send(command, function(err, data) {
    if (err) return callback(err);
    var anomalies = data.Anomalies.map(function(a) {
      return {
        id: a.AnomalyId,
        service: a.DimensionValue,
        impact: a.Impact.TotalImpact,
        startDate: a.AnomalyStartDate,
        endDate: a.AnomalyEndDate
      };
    });
    callback(null, anomalies);
  });
}

Organization-Level Cost Management

If you are running AWS Organizations with multiple accounts, cost management becomes more complex but also more powerful.

Consolidated Billing

All accounts in an organization roll up to the management (payer) account. This gives you volume discounts across all accounts and a single place to view total spending.

Organizational Budgets

Create budgets at the organization level that filter by linked account, organizational unit (OU), or tag. This lets you set per-team or per-environment spending limits.

Service Control Policies for Cost Control

SCPs can enforce cost guardrails. For example, block developers from launching expensive instance types in sandbox accounts:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyExpensiveInstances",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "ForAnyValue:StringLike": {
          "ec2:InstanceType": [
            "p4d.*",
            "p3.*",
            "x2idn.*",
            "*.24xlarge",
            "*.metal"
          ]
        }
      }
    }
  ]
}

Complete Working Example: Cost Monitoring Dashboard

This Node.js application pulls Cost Explorer data, checks budget thresholds, and sends Slack alerts when spending exceeds defined limits.

Project Setup

mkdir aws-cost-monitor
cd aws-cost-monitor
npm init -y
npm install @aws-sdk/client-cost-explorer @aws-sdk/client-budgets axios express

Main Application

// cost-monitor.js
var AWS = require("@aws-sdk/client-cost-explorer");
var BudgetsSDK = require("@aws-sdk/client-budgets");
var axios = require("axios");
var express = require("express");

var ceClient = new AWS.CostExplorerClient({ region: "us-east-1" });
var budgetsClient = new BudgetsSDK.BudgetsClient({ region: "us-east-1" });

var SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
var AWS_ACCOUNT_ID = process.env.AWS_ACCOUNT_ID;
var MONTHLY_BUDGET = parseFloat(process.env.MONTHLY_BUDGET || "5000");
var ALERT_THRESHOLD = parseFloat(process.env.ALERT_THRESHOLD || "0.8");
var CHECK_INTERVAL_MS = parseInt(process.env.CHECK_INTERVAL_MS || "21600000"); // 6 hours

var app = express();
var latestReport = null;

// Get cost breakdown by service for a date range
function getCostByService(startDate, endDate, callback) {
  var params = {
    TimePeriod: { Start: startDate, End: endDate },
    Granularity: "DAILY",
    Metrics: ["UnblendedCost"],
    GroupBy: [{ Type: "DIMENSION", Key: "SERVICE" }]
  };

  var command = new AWS.GetCostAndUsageCommand(params);
  ceClient.send(command, function(err, data) {
    if (err) return callback(err);

    var serviceTotals = {};
    data.ResultsByTime.forEach(function(period) {
      period.Groups.forEach(function(group) {
        var service = group.Keys[0];
        var cost = parseFloat(group.Metrics.UnblendedCost.Amount);
        serviceTotals[service] = (serviceTotals[service] || 0) + cost;
      });
    });

    var sorted = Object.keys(serviceTotals)
      .map(function(service) {
        return { service: service, cost: serviceTotals[service] };
      })
      .sort(function(a, b) { return b.cost - a.cost; })
      .slice(0, 15);

    callback(null, sorted);
  });
}

// Get total month-to-date spending
function getMonthToDateTotal(callback) {
  var now = new Date();
  var firstOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);

  var params = {
    TimePeriod: {
      Start: firstOfMonth.toISOString().split("T")[0],
      End: now.toISOString().split("T")[0]
    },
    Granularity: "MONTHLY",
    Metrics: ["UnblendedCost"]
  };

  var command = new AWS.GetCostAndUsageCommand(params);
  ceClient.send(command, function(err, data) {
    if (err) return callback(err);

    var total = 0;
    data.ResultsByTime.forEach(function(period) {
      total += parseFloat(period.Total.UnblendedCost.Amount);
    });

    // Calculate projected month-end cost
    var dayOfMonth = now.getDate();
    var daysInMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0).getDate();
    var projected = (total / dayOfMonth) * daysInMonth;

    callback(null, {
      monthToDate: total.toFixed(2),
      projected: projected.toFixed(2),
      dayOfMonth: dayOfMonth,
      daysInMonth: daysInMonth
    });
  });
}

// Get budget status
function getBudgetStatus(callback) {
  var params = { AccountId: AWS_ACCOUNT_ID, MaxResults: 20 };
  var command = new BudgetsSDK.DescribeBudgetsCommand(params);

  budgetsClient.send(command, function(err, data) {
    if (err) return callback(err);

    var budgets = data.Budgets.map(function(b) {
      var limit = parseFloat(b.BudgetLimit.Amount);
      var actual = b.CalculatedSpend
        ? parseFloat(b.CalculatedSpend.ActualSpend.Amount)
        : 0;
      var forecasted = b.CalculatedSpend && b.CalculatedSpend.ForecastedSpend
        ? parseFloat(b.CalculatedSpend.ForecastedSpend.Amount)
        : 0;

      return {
        name: b.BudgetName,
        limit: limit.toFixed(2),
        actual: actual.toFixed(2),
        forecasted: forecasted.toFixed(2),
        utilizationPercent: ((actual / limit) * 100).toFixed(1)
      };
    });

    callback(null, budgets);
  });
}

// Send Slack notification
function sendSlackAlert(message, callback) {
  if (!SLACK_WEBHOOK_URL) {
    console.log("[SLACK DISABLED] " + message);
    return callback(null);
  }

  axios.post(SLACK_WEBHOOK_URL, {
    text: message,
    username: "AWS Cost Monitor",
    icon_emoji: ":money_with_wings:"
  }).then(function() {
    callback(null);
  }).catch(function(err) {
    callback(err);
  });
}

// Build cost report and check thresholds
function runCostCheck() {
  console.log("[" + new Date().toISOString() + "] Running cost check...");

  var now = new Date();
  var firstOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);

  getMonthToDateTotal(function(err, totals) {
    if (err) {
      console.error("Failed to get MTD total:", err.message);
      return;
    }

    getCostByService(
      firstOfMonth.toISOString().split("T")[0],
      now.toISOString().split("T")[0],
      function(err, services) {
        if (err) {
          console.error("Failed to get service breakdown:", err.message);
          return;
        }

        getBudgetStatus(function(err, budgets) {
          if (err) {
            console.error("Failed to get budget status:", err.message);
            budgets = [];
          }

          latestReport = {
            timestamp: new Date().toISOString(),
            totals: totals,
            topServices: services,
            budgets: budgets
          };

          // Check if spending exceeds threshold
          var mtd = parseFloat(totals.monthToDate);
          var threshold = MONTHLY_BUDGET * ALERT_THRESHOLD;

          if (mtd > threshold) {
            var pct = ((mtd / MONTHLY_BUDGET) * 100).toFixed(1);
            var msg = "*AWS Cost Alert*\n"
              + "Month-to-date: $" + totals.monthToDate + "\n"
              + "Budget: $" + MONTHLY_BUDGET.toFixed(2) + "\n"
              + "Utilization: " + pct + "%\n"
              + "Projected month-end: $" + totals.projected + "\n\n"
              + "*Top 5 Services:*\n";

            services.slice(0, 5).forEach(function(s) {
              msg += "  - " + s.service + ": $" + s.cost.toFixed(2) + "\n";
            });

            sendSlackAlert(msg, function(err) {
              if (err) console.error("Slack alert failed:", err.message);
              else console.log("Alert sent to Slack");
            });
          } else {
            console.log("Spending within budget: $" + totals.monthToDate
              + " / $" + MONTHLY_BUDGET.toFixed(2));
          }
        });
      }
    );
  });
}

// API endpoints for the dashboard
app.get("/api/report", function(req, res) {
  if (!latestReport) {
    return res.status(503).json({ error: "Report not yet generated" });
  }
  res.json(latestReport);
});

app.get("/api/cost-history", function(req, res) {
  var days = parseInt(req.query.days) || 30;
  var now = new Date();
  var start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);

  var params = {
    TimePeriod: {
      Start: start.toISOString().split("T")[0],
      End: now.toISOString().split("T")[0]
    },
    Granularity: "DAILY",
    Metrics: ["UnblendedCost"]
  };

  var command = new AWS.GetCostAndUsageCommand(params);
  ceClient.send(command, function(err, data) {
    if (err) {
      return res.status(500).json({ error: err.message });
    }

    var history = data.ResultsByTime.map(function(period) {
      return {
        date: period.TimePeriod.Start,
        cost: parseFloat(period.Total.UnblendedCost.Amount).toFixed(2)
      };
    });

    res.json(history);
  });
});

app.get("/health", function(req, res) {
  res.json({ status: "ok", lastCheck: latestReport ? latestReport.timestamp : null });
});

// Start the server and schedule checks
var port = process.env.PORT || 3000;
app.listen(port, function() {
  console.log("Cost monitor running on port " + port);
  runCostCheck(); // initial check
  setInterval(runCostCheck, CHECK_INTERVAL_MS);
});

Running the Monitor

export AWS_ACCOUNT_ID="123456789012"
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00/B00/xxx"
export MONTHLY_BUDGET="5000"
export ALERT_THRESHOLD="0.8"

node cost-monitor.js

The dashboard exposes:

  • GET /api/report — Latest cost summary with service breakdown and budget status
  • GET /api/cost-history?days=30 — Daily cost history for charting
  • GET /health — Health check with last report timestamp

Common Issues and Troubleshooting

1. Cost Explorer API Returns Empty Results

Error: An error occurred (DataUnavailableException): Cost Explorer has not been enabled for this account

Fix: Enable Cost Explorer in the AWS Console under Billing > Cost Explorer. Wait 24 hours for data to populate. This is a per-account setting — enabling it in the management account does not enable it in linked accounts.

2. Budget Creation Fails with AccessDenied

Error: AccessDeniedException: User: arn:aws:iam::123456789012:user/deploy is not authorized to perform: budgets:CreateBudget

Fix: The IAM user needs the budgets:* permissions. Budgets API operates in us-east-1 only. Make sure your IAM policy is not region-restricted:

{
  "Effect": "Allow",
  "Action": "budgets:*",
  "Resource": "arn:aws:budgets::123456789012:budget/*"
}

Note the double colon — budgets ARNs do not include a region.

3. Cost Allocation Tags Not Showing in Cost Explorer

Filtering by tag "Project" returns zero results despite resources being tagged

Fix: User-defined tags must be activated in Billing > Cost allocation tags. After activation, tags only apply to costs incurred from that point forward — they are not retroactive. If you activated the tag yesterday, you will not see it on last week's costs.

4. CloudWatch Billing Alarm Never Triggers

Alarm state: INSUFFICIENT_DATA

Fix: Three common causes:

  1. "Receive Billing Alerts" is not enabled in Billing preferences.
  2. The alarm was created in a region other than us-east-1. Billing metrics only publish to us-east-1.
  3. The evaluation period is too short. Billing metrics update approximately every 6 hours (not real-time). Set the period to at least 21600 seconds (6 hours).

5. Cost Explorer Rate Limiting

Error: ThrottlingException: Rate exceeded for GetCostAndUsage

Fix: Cost Explorer has a limit of 5 requests per second. Implement exponential backoff:

function callWithRetry(fn, maxRetries, callback) {
  var attempts = 0;

  function attempt() {
    fn(function(err, data) {
      if (err && err.name === "ThrottlingException" && attempts < maxRetries) {
        attempts++;
        var delay = Math.pow(2, attempts) * 1000 + Math.random() * 500;
        console.log("Throttled. Retrying in " + delay + "ms...");
        setTimeout(attempt, delay);
      } else {
        callback(err, data);
      }
    });
  }

  attempt();
}

Best Practices

  • Set up billing alerts on day one. Before you deploy anything, create a budget with alerts at 50%, 80%, and 100%. This is non-negotiable for any AWS account.

  • Tag everything at creation time. Retrofitting tags is painful. Use CloudFormation or Terraform to enforce tags in your infrastructure templates. Make tag compliance part of your CI/CD pipeline.

  • Review Cost Explorer weekly. Do not wait for the monthly bill. A 10-minute weekly review of your top 5 services by cost catches problems early. Set a recurring calendar event.

  • Use Savings Plans over Reserved Instances for new commitments. Unless you have a very specific, unchanging workload, the flexibility of Compute Savings Plans outweighs the slightly higher price compared to RIs.

  • Delete unused resources aggressively. Unattached EBS volumes, idle Elastic IPs, orphaned NAT Gateways, and forgotten RDS snapshots add up. Run a monthly cleanup using AWS Trusted Advisor or a tool like cloud-nuke.

  • Put your cost monitoring in code. The programmatic approach shown in this article catches things the console does not. Automated checks running every 6 hours with Slack alerts are more reliable than hoping someone remembers to check the billing dashboard.

  • Use VPC endpoints for high-traffic internal services. NAT Gateway data processing charges ($0.045/GB) are one of the most common bill surprises. A VPC endpoint for S3 or DynamoDB costs pennies by comparison.

  • Right-size before committing. Run Compute Optimizer for at least 14 days before purchasing Savings Plans. Committing to a dollar amount based on oversized instances means you are locking in waste.

  • Separate accounts by environment. Use AWS Organizations with dedicated accounts for production, staging, and development. This gives you natural cost boundaries and prevents development experimentation from polluting your production cost data.

References

Powered by Contentful