Cost Analysis Is the Starting Point for Every Optimization Decision
Azure Cost Management provides a powerful query engine for investigating where money goes. But most teams use it at a surface level — opening the default view, glancing at the top-level chart, and closing the tab. The real optimization value comes from understanding the filtering and grouping system well enough to ask precise questions about your spend. This guide covers every filter dimension, view configuration, and query pattern you need to turn Cost Analysis from a reporting tool into an optimization engine.
Understanding the Filter Dimensions
Cost Analysis supports over 30 filter dimensions. Each dimension lets you slice cost data along a different axis. Knowing which dimension to use for which question is the foundation of effective cost analysis.
Resource-Level Dimensions
| Dimension | What It Tells You | Optimization Use |
|---|---|---|
| Resource | Individual resource name | Find top-spending resources for right-sizing |
| ResourceId | Full ARM resource ID | Programmatic identification for automation |
| Resource group | Resource group name | Team/project-level cost attribution |
| Resource type | ARM resource type (e.g., Microsoft.Compute/virtualMachines) | Category-level spend analysis |
| Location | Azure region | Identify cross-region cost differences and optimization by region selection |
Service and Meter Dimensions
| Dimension | What It Tells You | Optimization Use |
|---|---|---|
| ServiceName | Azure service classification | Identify which services drive the most cost |
| ServiceFamily | Service family (Compute, Analytics, Security) | High-level spending category analysis |
| Meter category | Service breakdown by meter | Distinguish compute vs. storage vs. networking costs within a service |
| Meter subcategory | Further meter classification | Identify specific SKU tiers generating cost |
| Meter | Individual usage meter | Root-cause analysis of cost changes at the meter level |
Pricing and Purchase Dimensions
| Dimension | What It Tells You | Optimization Use |
|---|---|---|
| Pricing model | OnDemand, Reservation, Spot, Savings Plan | Measure reservation and savings plan coverage |
| Charge type | Usage, purchase, refund, unused reservation | Find wasted reservation spend (amortized view) |
| Frequency | OneTime, Recurring, UsageBased | Separate one-time purchases from recurring charges |
| Publisher type | Microsoft vs. Marketplace | Track third-party Marketplace spending separately |
Organizational Dimensions
| Dimension | What It Tells You | Optimization Use |
|---|---|---|
| Subscription | Azure subscription | Per-subscription cost tracking and comparison |
| Department | EA department | Business unit cost allocation (EA only) |
| BillingProfileName | Billing profile | Invoice-level cost segmentation |
| Tag | Resource tag key:value pairs | Custom cost allocation by environment, team, project, cost center |
Practical Filter Patterns for Optimization
Cost Analysis does not support grouping by multiple dimensions simultaneously. The workaround is combining a filter on one dimension with a group-by on another. This filter-then-group pattern is the core technique for targeted cost investigation.
Pattern 1: Find Top-Spending Resources Per Service
- Open Cost Analysis → select Cost by service smart view
- Identify the highest-cost service (for example, Virtual Machines)
- Click the service slice to apply it as a filter (or add filter manually: ServiceName = Virtual Machines)
- Change Group by to Resource
- Sort by cost to identify the top-spending VMs for right-sizing evaluation
Pattern 2: Identify Unused Reservations
- Switch to Amortized cost view (bottom of the view selector or toggle in customization)
- Add filter: Charge type = UnusedReservation
- Group by Reservation
- The result shows each reservation with unused capacity and the dollar amount wasted
This pattern only works in the amortized view. In actual cost view, reservation purchases appear as lump sums on the purchase date, and unused capacity is invisible.
Pattern 3: Compare On-Demand vs. Reserved Pricing
- Group by Pricing model
- The stacked chart shows OnDemand, Reservation, Savings Plan, and Spot segments
- A large OnDemand segment relative to total compute indicates reservation purchase opportunities
- Cross-reference with Azure Advisor reservation recommendations for specific purchase suggestions
Pattern 4: Track Costs by Environment Tag
- Add filter: Tag → select your environment tag key (e.g., “environment”)
- Select values: Production, Development, Staging
- Group by ServiceName to see which services cost the most in each environment
- Untagged resources appear in a separate group — these represent your tagging compliance gap
Tags are only included in cost data while actively applied and the resource is running. Tags are not applied retroactively. Enable tag inheritance in Cost Management settings to have resource group and subscription tags flow down to child resource usage records automatically.
Pattern 5: Spot Resources Running 24/7
- Set granularity to Daily
- Group by Resource
- Filter to a specific resource group or service
- Look for resources with consistent daily costs seven days a week — these might be dev/test workloads that should only run during business hours
Actual vs. Amortized Cost: When to Use Each
Choosing the wrong cost view leads to incorrect optimization conclusions. The two views answer fundamentally different questions.
| Aspect | Actual Cost | Amortized Cost |
|---|---|---|
| Reservation purchases | Full amount on purchase date | Spread evenly over the term (daily or monthly) |
| Unused reservations | Not visible | Shown as UnusedReservation charge type |
| Best for | Invoice reconciliation, cash flow tracking | True resource cost, chargeback, finding wasted reservations |
| Monthly pattern | Purchase month shows large spike | Costs distributed evenly across months |
For optimization work, always use amortized cost. It reveals the true cost of running each resource (including the amortized portion of reservations) and exposes unused reservation capacity that actual cost hides.
Granularity Selection Strategy
Cost Analysis supports three granularity levels, each with specific limits and use cases:
| Granularity | Maximum Date Range | Best For |
|---|---|---|
| Daily | 3 months (92 days); 1 month at management group scope | Spike investigation, usage patterns, identifying weekend/overnight waste |
| Monthly | 12 months | Trend analysis, month-over-month comparisons, budget tracking |
| None (accumulated) | 12 months | Summary totals, forecast visualization, budget vs. actual comparison |
If you select daily granularity with a date range exceeding 92 days, Cost Analysis automatically switches to monthly granularity and adjusts the dates to align with full calendar months. For cost spike investigation, always start with daily granularity to pinpoint the exact day of the cost change.
Saved Views for Recurring Analysis
Once you build a useful filter configuration, save it to avoid recreating it every time. Each user can create up to 100 private views across all scopes, and each scope supports up to 100 shared views visible to anyone with Cost Management Reader access.
Creating a Saved View
- Configure your filters, grouping, granularity, and chart type
- Select Save as to create a new view (or Save to update the current one)
- Name the view descriptively (e.g., “Prod VM Costs – Daily” or “Unused Reservations – Monthly”)
- Choose Private (only you) or Shared (visible to anyone with Reader access)
The saved view stores only the query configuration, not the underlying data. Each time you open the view, it executes a fresh query against current cost data.
Subscribing to Scheduled Email Reports
Turn any saved view into an automated email report:
- Open the saved view
- Select Subscribe from the toolbar
- Set the frequency: Daily, Weekly (specific day), or Monthly (specific day)
- Add email recipients (up to 20)
- Optionally include a CSV data attachment along with the chart image
This creates a scheduled action that emails the view’s chart and optionally the raw data on your chosen cadence. Use this for weekly cost reviews or monthly showback reports without requiring portal access.
Querying Costs Programmatically with Filters
REST API Query with Filters
The Cost Management Query API accepts dimension and tag filters for programmatic cost analysis. This enables building custom reports, integrations, and automated optimization workflows.
POST https://management.azure.com/subscriptions/{subscriptionId}/providers/Microsoft.CostManagement/query?api-version=2025-03-01
{
"type": "AmortizedCost",
"timeframe": "TheLastMonth",
"dataset": {
"granularity": "Daily",
"aggregation": {
"totalCost": {
"name": "PreTaxCost",
"function": "Sum"
}
},
"grouping": [
{
"name": "ResourceGroup",
"type": "Dimension"
}
],
"filter": {
"and": [
{
"dimensions": {
"name": "ServiceName",
"operator": "In",
"values": ["Virtual Machines", "Storage"]
}
},
{
"tags": {
"name": "environment",
"operator": "In",
"values": ["production"]
}
}
]
}
}
}
The API supports up to 2 grouping clauses and 2 aggregation clauses per query. The only filter operator available is In. Filter expressions support and and or logical operators, each requiring at least two child expressions.
PowerShell Queries with Filters
# Query costs filtered by resource group
$dimensionFilter = New-AzCostManagementQueryComparisonExpressionObject `
-Name 'ResourceGroup' `
-Value @('rg-prod-compute', 'rg-prod-data')
$filter = New-AzCostManagementQueryFilterObject -Dimensions $dimensionFilter
$result = Invoke-AzCostManagementQuery `
-Scope "/subscriptions/$subscriptionId" `
-Timeframe TheLastMonth `
-Type AmortizedCost `
-DatasetGranularity Monthly `
-DatasetFilter $filter
# Parse results
$columns = $result.Column
$result.Row | ForEach-Object {
$row = $_
$obj = [ordered]@{}
for ($i = 0; $i -lt $columns.Count; $i++) {
$obj[$columns[$i].Name] = $row[$i]
}
[PSCustomObject]$obj
} | Format-Table -AutoSize
Tag-Based Cost Report Script
# Generate a cost-by-environment report using tag filters
$environments = @("production", "staging", "development")
$subscriptionId = "your-subscription-id"
$report = @()
foreach ($env in $environments) {
$tagFilter = New-AzCostManagementQueryComparisonExpressionObject `
-Name 'environment' `
-Value @($env)
$filter = New-AzCostManagementQueryFilterObject -Tag $tagFilter
$result = Invoke-AzCostManagementQuery `
-Scope "/subscriptions/$subscriptionId" `
-Timeframe TheLastMonth `
-Type AmortizedCost `
-DatasetGranularity None `
-DatasetFilter $filter
$totalCost = ($result.Row | ForEach-Object { $_[0] } | Measure-Object -Sum).Sum
$report += [PSCustomObject]@{
Environment = $env
MonthlyCost = [math]::Round($totalCost, 2)
}
}
$report | Sort-Object MonthlyCost -Descending | Format-Table -AutoSize
# Output:
# Environment MonthlyCost
# ----------- -----------
# production 12847.33
# staging 3421.67
# development 1893.22
Building an Optimization Workflow with Filters
Combine filter patterns into a systematic monthly optimization review.
Week 1: Service-Level Review
- Open Cost Analysis with Monthly granularity, TheLastMonth timeframe
- Group by ServiceName
- Compare to the previous month — identify any service with more than 10% cost increase
- For each service with significant increase, drill down by filtering to that service and grouping by Resource
Week 2: Reservation and Savings Plan Health
- Switch to Amortized cost view
- Filter: Charge type = UnusedReservation
- Group by Reservation — quantify wasted reservation spend
- Group by Pricing model — calculate reservation and savings plan coverage percentage
- Review Azure Advisor recommendations for new reservation purchases
Week 3: Tag Compliance and Showback
- Group by your primary cost allocation tag (e.g., CostCenter)
- Measure the “Untagged” segment as a percentage of total spend
- If untagged spend exceeds your threshold (e.g., 5%), filter to untagged resources and group by Resource group to identify which teams need to improve tagging
- Generate per-team showback reports using tag filters
Week 4: Waste Identification
- Set granularity to Daily, timeframe to last 30 days
- Group by Resource for compute services
- Identify resources with flat daily costs (no weekday/weekend variation) — candidates for scheduled shutdown
- Cross-reference with Advisor shutdown recommendations (CPU < 3% P95)
- Check for orphaned resources: disks without VMs, public IPs without NICs, idle load balancers
Common Mistakes That Undermine Cost Analysis
- Using actual cost for optimization — Actual cost hides reservation waste and distorts monthly comparisons when purchases occur. Always use amortized cost for optimization analysis.
- Ignoring the “Others” group — Cost Analysis shows the top 9 items plus an “Others” bucket. If “Others” is a significant portion of total cost, you are missing important detail. Add a filter to reduce the result set so the top 9 covers meaningful items.
- Not using daily granularity for spike investigation — Monthly granularity masks daily patterns. A resource running 24/7 costs the same monthly as one running only weekdays if the monthly total is the same, but daily granularity reveals the weekday-only pattern.
- Trusting tags without checking compliance — Tag-based cost reports are only accurate when tagging compliance is high. Always measure the untagged percentage before trusting tag-based showback numbers.
- Querying the API without caching — The Query API has strict rate limits (12 QPU per 10 seconds). Building a dashboard that queries on every page load will hit these limits with modest user counts. Cache results server-side with a 4-hour TTL.
Automating Cost Analysis Reports
Schedule automated cost reports using the Scheduled Actions API to deliver filtered views directly to stakeholders’ inboxes without requiring portal access.
# Create a scheduled weekly cost report for the production team
$token = (Get-AzAccessToken -ResourceUrl "https://management.azure.com").Token
$subscriptionId = "your-subscription-id"
$body = @{
kind = "Email"
properties = @{
displayName = "Weekly Production Cost Report"
notification = @{
subject = "Weekly Azure Cost Report - Production"
to = @("prod-team@contoso.com", "finops@contoso.com")
message = "Automated weekly cost report for production resources"
}
schedule = @{
frequency = "Weekly"
dayOfWeek = "Monday"
startDate = (Get-Date -Format "yyyy-MM-ddT00:00:00Z")
endDate = (Get-Date).AddYears(1).ToString("yyyy-MM-ddT00:00:00Z")
}
status = "Enabled"
viewId = "/subscriptions/$subscriptionId/providers/Microsoft.CostManagement/views/prod-weekly-review"
}
} | ConvertTo-Json -Depth 5
Invoke-RestMethod `
-Uri "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.CostManagement/scheduledActions/weekly-prod-report?api-version=2025-03-01" `
-Method PUT `
-Headers @{ Authorization = "Bearer $token" } `
-Body $body `
-ContentType "application/json"
The viewId references a saved view that contains your filter configuration. Create the view with the desired filters first (either in the portal or via API), then reference it in the scheduled action. This decouples the report schedule from the query configuration — update the view’s filters without touching the schedule.
For more details, refer to the official documentation: What is Microsoft Cost Management.