← Back to DevBytes

Monitoring Blob Storage: Metrics, Alarms, and Dashboards

Introduction to Blob Storage Monitoring

Blob storage has become the backbone of modern cloud applications—hosting everything from static website assets and media files to data lakehouses and backup archives. As your reliance on blob storage grows, so does the need to understand exactly how it's performing, who's accessing it, and whether it's operating within expected cost and performance boundaries. Monitoring blob storage means systematically collecting, analyzing, and acting upon metrics, logs, and traces emitted by your storage service. This tutorial covers the end-to-end workflow: understanding available metrics, configuring collection, setting up intelligent alarms, and building dashboards that give you real-time visibility into your storage estate.

What Blob Storage Monitoring Entails

At its core, monitoring blob storage involves three interconnected pillars:

Whether you're using Azure Blob Storage, AWS S3, or Google Cloud Storage, the principles are universal. This guide uses Azure Blob Storage and Azure Monitor as the primary reference, with code examples adaptable across cloud providers.

Why Monitoring Matters

Unmonitored blob storage is a ticking time bomb. Here's what effective monitoring prevents:

Understanding Blob Storage Metrics

Key Metrics Categories

Azure Blob Storage exposes metrics through Azure Monitor in two broad categories:

Understanding the distinction is critical because capacity metrics have a 24-hour latency—they are not suitable for real-time alerting but are invaluable for trend analysis and cost forecasting.

Transaction Metrics Deep Dive

The following transaction metrics form the backbone of operational monitoring. Each is available per blob service (blob, queue, table if applicable) and per API operation group:

Capacity Metrics Deep Dive

Enabling Metrics and Diagnostic Logs

Step 1: Enable Platform Metrics

Platform metrics for Azure Blob Storage are enabled by default and retained for 93 days at no additional cost. You can verify this in the Azure Portal under your storage account's Monitoring → Metrics blade. No action is required for basic metric collection.

Step 2: Enable Diagnostic Settings for Rich Logs

While platform metrics give you numeric aggregates, diagnostic logs provide granular per-request detail—including caller IP, operation name, request headers, and full response codes. These are routed to a Log Analytics workspace, Storage account, Event Hub, or third-party SIEM.

Here's how to enable diagnostic settings via Azure CLI:

# Login and select subscription
az login
az account set --subscription "your-subscription-id"

# Create a Log Analytics workspace (if needed)
az monitor log-analytics workspace create \
  --resource-group "rg-monitoring" \
  --workspace-name "blob-logs-workspace" \
  --location "eastus"

# Enable diagnostic settings for blob storage
az monitor diagnostic-settings create \
  --resource "/subscriptions/sub-id/resourceGroups/rg-storage/providers/Microsoft.Storage/storageAccounts/mystorageaccount" \
  --name "blob-diag" \
  --workspace "/subscriptions/sub-id/resourceGroups/rg-monitoring/providers/Microsoft.OperationalInsights/workspaces/blob-logs-workspace" \
  --logs '[
    {
      "category": "StorageRead",
      "enabled": true,
      "retentionPolicy": {"enabled": true, "days": 30}
    },
    {
      "category": "StorageWrite",
      "enabled": true,
      "retentionPolicy": {"enabled": true, "days": 30}
    },
    {
      "category": "StorageDelete",
      "enabled": true,
      "retentionPolicy": {"enabled": true, "days": 30}
    }
  ]' \
  --metrics '[
    {
      "category": "Transaction",
      "enabled": true,
      "retentionPolicy": {"enabled": true, "days": 30}
    }
  ]'

For an ARM template or Bicep deployment, the equivalent configuration in JSON looks like this snippet you'd include in your storage account resource:

{
  "type": "Microsoft.Insights/diagnosticSettings",
  "apiVersion": "2021-05-01-preview",
  "name": "[concat('blob-diag-', parameters('storageAccountName'))]",
  "dependsOn": [
    "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageAccountName'))]"
  ],
  "properties": {
    "workspaceId": "[parameters('logAnalyticsWorkspaceId')]",
    "logs": [
      {
        "category": "StorageRead",
        "enabled": true,
        "retentionPolicy": { "enabled": true, "days": 30 }
      },
      {
        "category": "StorageWrite",
        "enabled": true,
        "retentionPolicy": { "enabled": true, "days": 30 }
      },
      {
        "category": "StorageDelete",
        "enabled": true,
        "retentionPolicy": { "enabled": true, "days": 30 }
      }
    ],
    "metrics": [
      {
        "category": "Transaction",
        "enabled": true
      }
    ]
  }
}

Step 3: Enable Hierarchical Namespace Logs (Data Lake Storage Gen2)

If your storage account has hierarchical namespace enabled (ADLS Gen2), you should also capture the StorageRead, StorageWrite, and StorageDelete categories for the DFS endpoint. The configuration mirrors the blob endpoint—just ensure the diagnostic setting targets the same workspace.

Querying Metrics and Logs with Kusto (KQL)

Once diagnostic logs flow into your Log Analytics workspace, you can use Kusto Query Language to extract powerful insights. Here are essential queries every operator should have in their toolkit:

Top 10 Containers by Read Volume (Last 24 Hours)

StorageBlobLogs
| where TimeGenerated >= ago(24h)
| where OperationName == "GetBlob"
| summarize TotalReads = count(), TotalEgressBytes = sum(ResponseBodySize) by ContainerName = ObjectKey
| top 10 by TotalReads desc
| project ContainerName, TotalReads, TotalEgressBytesGB = round(TotalEgressBytes / 1073741824, 2)

Throttled Requests Breakdown by Minute

StorageBlobLogs
| where TimeGenerated >= ago(1h)
| where StatusCode == 429
| summarize ThrottledCount = count() by bin(TimeGenerated, 1m), OperationName
| order by TimeGenerated asc
| render timechart

Anonymous Access Attempts (Potential Security Issue)

StorageBlobLogs
| where TimeGenerated >= ago(7d)
| where AuthenticationType == "Anonymous"
| summarize RequestCount = count() by bin(TimeGenerated, 1h), CallerIpAddress
| where RequestCount > 10
| order by RequestCount desc

Latency Percentiles for Critical Operations

StorageBlobLogs
| where TimeGenerated >= ago(24h)
| where OperationName in ("GetBlob", "PutBlob", "DeleteBlob")
| summarize
    P50 = percentile(ServerLatencyMs, 50),
    P95 = percentile(ServerLatencyMs, 95),
    P99 = percentile(ServerLatencyMs, 99),
    AvgLatencyMs = avg(ServerLatencyMs),
    RequestCount = count()
    by OperationName

Creating Alarms and Alerts

The Anatomy of an Alert Rule

An Azure Monitor alert rule binds a metric signal to an action group. The key components are:

Creating a Static Threshold Alert via Azure CLI

This alert triggers when the total egress exceeds 10 GB in a 5-minute window—useful for catching unexpected data transfer spikes that might indicate exfiltration or runaway application behavior:

# First, create an action group for email notification
az monitor action-group create \
  --resource-group "rg-monitoring" \
  --name "ops-team-email" \
  --short-name "opsemail" \
  --action email ops-team-email ops-team@example.com

# Get the action group ID
ACTION_GROUP_ID=$(az monitor action-group show \
  --resource-group "rg-monitoring" \
  --name "ops-team-email" \
  --query id -o tsv)

# Create the alert rule
az monitor metrics alert create \
  --resource-group "rg-monitoring" \
  --name "HighEgressAlert" \
  --scopes "/subscriptions/sub-id/resourceGroups/rg-storage/providers/Microsoft.Storage/storageAccounts/mystorageaccount" \
  --description "Alert when blob egress exceeds 10 GB in 5 minutes" \
  --condition "TotalEgress > 10737418240" \
  --aggregation "Total" \
  --window-size "5m" \
  --evaluation-frequency "5m" \
  --severity 2 \
  --action "$ACTION_GROUP_ID"

Creating a Dynamic Threshold Alert (Anomaly Detection)

Dynamic thresholds use machine learning to establish a baseline and alert only on statistically significant deviations. This is ideal for metrics with seasonal patterns—like nightly backup traffic or weekly data ingestion jobs:

az monitor metrics alert create \
  --resource-group "rg-monitoring" \
  --name "ThrottlingAnomalyAlert" \
  --scopes "/subscriptions/sub-id/resourceGroups/rg-storage/providers/Microsoft.Storage/storageAccounts/mystorageaccount" \
  --description "Alert on anomalous throttling rate" \
  --condition "ThrottlingRate > dynamic" \
  --dynamic-threshold-sensitivity "High" \
  --dynamic-threshold-operator "GreaterThan" \
  --aggregation "Average" \
  --window-size "15m" \
  --evaluation-frequency "15m" \
  --severity 1 \
  --action "$ACTION_GROUP_ID"

Log-Based Alert: Data Exfiltration Detection

For sophisticated scenarios, log query alerts let you trigger on patterns that raw metrics can't capture. This example alerts when a single IP downloads more than 500 distinct blobs in an hour—a classic exfiltration signature:

# Create a log-based alert rule
az monitor scheduled-query create \
  --resource-group "rg-monitoring" \
  --name "DataExfiltrationPattern" \
  --scopes "/subscriptions/sub-id/resourceGroups/rg-monitoring/providers/Microsoft.OperationalInsights/workspaces/blob-logs-workspace" \
  --description "Alert when a single IP downloads > 500 blobs in an hour" \
  --query '
StorageBlobLogs
| where TimeGenerated >= ago(1h)
| where OperationName == "GetBlob"
| where StatusCode == 200
| summarize DistinctBlobCount = dcount(ObjectKey), TotalEgressMB = sum(ResponseBodySize) / 1048576 by CallerIpAddress
| where DistinctBlobCount > 500
' \
  --trigger-operator "GreaterThan" \
  --trigger-threshold 0 \
  --frequency "60m" \
  --severity 1 \
  --action "$ACTION_GROUP_ID"

Alert on Capacity Growth for Cost Control

Capacity metrics are sampled once daily. To alert on unexpected storage growth (e.g., a 20% increase day-over-day), you can use a scheduled query alert that compares the current day's capacity to the previous day's:

# KQL query for the alert rule
let CurrentDay = toscalar(StorageBlobLogs | summarize arg_max(TimeGenerated, *) | project startofday(TimeGenerated));
let PreviousDay = CurrentDay - 1d;
let CurrentCapacity = toscalar(
  AzureMetrics
  | where MetricName == "BlobCapacity"
  | where TimeGenerated >= CurrentDay and TimeGenerated < CurrentDay + 1d
  | summarize arg_max(TimeGenerated, Total)
  | project Total
);
let PreviousCapacity = toscalar(
  AzureMetrics
  | where MetricName == "BlobCapacity"
  | where TimeGenerated >= PreviousDay and TimeGenerated < PreviousDay + 1d
  | summarize arg_max(TimeGenerated, Total)
  | project Total
);
print GrowthPercent = round((CurrentCapacity - PreviousCapacity) / PreviousCapacity * 100, 2)
| where GrowthPercent > 20

Building Dashboards for Blob Storage Monitoring

Azure Monitor Workbooks

Workbooks are the most flexible dashboarding tool in Azure Monitor. They combine text, visualizations, and KQL queries into interactive reports. Here's a workbook template JSON snippet that creates a comprehensive blob storage overview. Save this as a .json file and import it via the Azure Portal or deploy it via ARM:

{
  "version": "Notebook/1.0",
  "items": [
    {
      "type": 1,
      "content": {
        "json": "# Blob Storage Health Dashboard\n## Overview of transactions, latency, and throttling for the past 24 hours"
      },
      "name": "text - title"
    },
    {
      "type": 3,
      "content": {
        "version": "KqlItem/1.0",
        "query": "StorageBlobLogs\r\n| where TimeGenerated >= ago(24h)\r\n| summarize RequestCount = count() by bin(TimeGenerated, 1h), StatusClass = case(\r\n    StatusCode between (200 .. 299), \"Success\",\r\n    StatusCode between (400 .. 499), \"ClientError\",\r\n    StatusCode between (500 .. 599), \"ServerError\",\r\n    \"Other\"\r\n)\r\n| render timechart",
        "size": 0,
        "title": "Request Volume by Status Class (Hourly)",
        "timeContext": { "durationMs": 86400000 },
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces"
      },
      "name": "query - request volume"
    },
    {
      "type": 3,
      "content": {
        "version": "KqlItem/1.0",
        "query": "StorageBlobLogs\r\n| where TimeGenerated >= ago(24h)\r\n| where OperationName in (\"GetBlob\", \"PutBlob\")\r\n| summarize P95LatencyMs = percentile(ServerLatencyMs, 95), AvgLatencyMs = avg(ServerLatencyMs) by bin(TimeGenerated, 1h), OperationName\r\n| render timechart",
        "size": 0,
        "title": "Server Latency P95 & Average (Hourly)",
        "timeContext": { "durationMs": 86400000 },
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces"
      },
      "name": "query - latency"
    },
    {
      "type": 3,
      "content": {
        "version": "KqlItem/1.0",
        "query": "StorageBlobLogs\r\n| where TimeGenerated >= ago(24h)\r\n| where StatusCode == 429\r\n| summarize ThrottledCount = count() by bin(TimeGenerated, 15m)\r\n| render areachart",
        "size": 0,
        "title": "Throttled Requests (429) - 15 Minute Buckets",
        "timeContext": { "durationMs": 86400000 },
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces"
      },
      "name": "query - throttling"
    },
    {
      "type": 3,
      "content": {
        "version": "KqlItem/1.0",
        "query": "StorageBlobLogs\r\n| where TimeGenerated >= ago(24h)\r\n| summarize TotalEgressGB = round(sum(ResponseBodySize) / 1073741824, 2), TotalIngressGB = round(sum(RequestBodySize) / 1073741824, 2) by Container = ObjectKey\r\n| top 10 by TotalEgressGB desc\r\n| project Container, TotalEgressGB, TotalIngressGB, TotalTrafficGB = round(TotalEgressGB + TotalIngressGB, 2)",
        "size": 1,
        "title": "Top 10 Containers by Data Transfer (24h)",
        "timeContext": { "durationMs": 86400000 },
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces"
      },
      "name": "query - top containers"
    },
    {
      "type": 9,
      "content": {
        "version": "KqlItem/1.0",
        "query": "StorageBlobLogs\r\n| where TimeGenerated >= ago(30d)\r\n| summarize TotalEgressGB = round(sum(ResponseBodySize) / 1073741824, 2) by bin(TimeGenerated, 1d)\r\n| render timechart",
        "size": 0,
        "title": "Daily Egress Trend (30 Days)",
        "timeContext": { "durationMs": 2592000000 },
        "queryType": 0,
        "resourceType": "microsoft.operationalinsights/workspaces"
      },
      "name": "query - egress trend"
    }
  ],
  "metadata": {
    "name": "Blob Storage Health Dashboard",
    "description": "Comprehensive monitoring dashboard for blob storage accounts"
  }
}

Custom Dashboard with Azure Portal Tiles

For quick at-a-glance monitoring, you can pin metric charts directly to an Azure Dashboard from the Metrics explorer. The following Azure CLI script creates a dashboard with pre-pinned tiles for key blob metrics:

# Create a new dashboard
az portal dashboard create \
  --resource-group "rg-monitoring" \
  --name "BlobOpsDashboard" \
  --location "eastus" \
  --tags "environment=production" "team=storage-ops" \
  --definition '
{
  "properties": {
    "lenses": [
      {
        "order": 0,
        "parts": [
          {
            "position": { "x": 0, "y": 0, "rowSpan": 4, "colSpan": 6 },
            "metadata": {
              "type": "Extension/Microsoft_Azure_Monitoring/PartType/MetricsChartPart",
              "settings": {
                "resourceId": "/subscriptions/sub-id/resourceGroups/rg-storage/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
                "metric": "TotalEgress",
                "aggregation": "Sum",
                "timeRange": "P7D",
                "title": "Total Egress (7 Days)"
              }
            }
          },
          {
            "position": { "x": 6, "y": 0, "rowSpan": 4, "colSpan": 6 },
            "metadata": {
              "type": "Extension/Microsoft_Azure_Monitoring/PartType/MetricsChartPart",
              "settings": {
                "resourceId": "/subscriptions/sub-id/resourceGroups/rg-storage/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
                "metric": "Transactions",
                "aggregation": "Sum",
                "timeRange": "P7D",
                "title": "Transaction Volume (7 Days)",
                "filter": "ResponseType eq 'Success'"
              }
            }
          },
          {
            "position": { "x": 0, "y": 4, "rowSpan": 4, "colSpan": 6 },
            "metadata": {
              "type": "Extension/Microsoft_Azure_Monitoring/PartType/MetricsChartPart",
              "settings": {
                "resourceId": "/subscriptions/sub-id/resourceGroups/rg-storage/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
                "metric": "E2ELatency",
                "aggregation": "Average",
                "timeRange": "PT24H",
                "title": "E2E Latency Average (24h)"
              }
            }
          },
          {
            "position": { "x": 6, "y": 4, "rowSpan": 4, "colSpan": 6 },
            "metadata": {
              "type": "Extension/Microsoft_Azure_Monitoring/PartType/MetricsChartPart",
              "settings": {
                "resourceId": "/subscriptions/sub-id/resourceGroups/rg-storage/providers/Microsoft.Storage/storageAccounts/mystorageaccount",
                "metric": "BlobCapacity",
                "aggregation": "Maximum",
                "timeRange": "P30D",
                "title": "Blob Capacity Trend (30 Days)"
              }
            }
          }
        ]
      }
    ]
  }
}'

Grafana Integration for Cross-Cloud Dashboards

If you operate across multiple clouds or prefer open-source tooling, Azure Monitor can be connected to Grafana. Here's a snippet configuring the Azure Monitor data source in a Grafana provisioning file:

# grafana/provisioning/datasources/azure-monitor.yaml
apiVersion: 1

datasources:
  - name: Azure Monitor
    type: grafana-azure-monitor-datasource
    access: proxy
    jsonData:
      cloudName: azuremonitor
      subscriptionId: "your-subscription-id"
      tenantId: "your-tenant-id"
      clientId: "your-service-principal-id"
      azureMonitorEndpoint: "https://portal.azure.com"
    secureJsonData:
      clientSecret: "your-service-principal-secret"
    version: 1
    editable: true

Once connected, you can create Grafana dashboard panels using KQL queries against your Log Analytics workspace, combining blob storage metrics with data from AWS CloudWatch or GCP Cloud Monitoring for a unified view.

Best Practices for Blob Storage Monitoring

1. Alert on Leading Indicators, Not Just Failures

Don't wait for 5xx server errors. Alert on rising throttling rates (429 responses), increasing P95 latency, and sudden egress spikes. These are leading indicators that predict imminent failures. Configure dynamic threshold alerts with "High" sensitivity for latency metrics to catch subtle degradation before it becomes user-visible.

2. Segment Alerts by Blob Tier and Operation

Archive tier operations have fundamentally different latency profiles than Hot tier. Create separate alert rules for different blob tiers and operation types. A 500ms latency on a PutBlob to Hot tier is alarming; the same latency on Archive retrieval is expected. Use dimension filters in your alert conditions to narrow scope.

3. Implement a Multi-Tier Notification Strategy

Not all alerts need to wake someone up at 3 AM. Structure your action groups into tiers:

4. Monitor Cost Metrics Proactively

Enable Azure Cost Management exports to your Log Analytics workspace and create dashboards that correlate storage capacity metrics with actual billed costs. Alert when the daily cost anomaly exceeds 15% of the 7-day rolling average. Tag storage accounts with cost-center metadata to enable chargeback reporting.

5. Retain Logs Strategically

Log Analytics workspace ingestion and retention costs can escalate quickly. Configure a retention policy that balances compliance needs with cost:

6. Automate Remediation with Action Groups

Action groups can trigger Azure Functions or webhooks. Use this to implement self-healing: when throttling alerts fire, automatically scale workloads to alternative storage accounts or enable request queuing. When capacity growth exceeds forecast, trigger a runbook that applies lifecycle management policies to transition aged blobs to cooler tiers.

7. Test Your Alert Rules Regularly

Alert rules that never fire breed complacency. Schedule quarterly "fire drills" where you artificially generate conditions that should trigger alerts (e.g., upload a large test file to spike egress, or send a burst of requests to trigger throttling). Verify that notifications arrive, escalation paths work, and runbook remediations execute correctly.

8. Correlate Application and Infrastructure Metrics

Blob storage metrics don't exist in isolation. When debugging a latency issue, correlate the storage-side E2E latency with application-side metrics (like HTTP request duration from your web servers). Use distributed tracing (via W3C Trace Context headers propagated through your storage SDK) to pinpoint whether the bottleneck is in the storage service, the network, or the application's serialization logic.

9. Use Metric Alerts Over Log Alerts Where Possible

Metric alerts evaluate faster (every 1 minute) and have lower latency than log alerts (which evaluate every 5-15 minutes). Use metric alerts for operational signals like throttling, latency, and error rates. Reserve log alerts for complex patterns that require KQL—like data exfiltration detection, anomalous access patterns, or multi-resource correlations.

10. Document Your Monitoring Architecture

Maintain a living document (or a monitoring-as-code repository) that captures every alert rule, its intended purpose, the expected response playbook, and the owning team. Use Terraform, Bicep, or ARM templates to version-control your monitoring infrastructure. A sample Terraform snippet for an alert rule:

resource "azurerm_monitor_metric_alert" "high_egress" {
  name                = "HighEgressAlert"
  resource_group_name = azurerm_resource_group.monitoring.name
  scopes              = [azurerm_storage_account.main.id]
  description         = "Alert when egress exceeds 10GB in 5 minutes"
  severity            = 2
  frequency           = "PT5M"
  window_size         = "PT5M"

  criteria {
    metric_namespace = "Microsoft.Storage/storageAccounts"
    metric_name      = "TotalEgress"
    aggregation      = "Total"
    operator         = "GreaterThan"
    threshold        = 10737418240
  }

  action {
    action_group_id = azurerm_monitor_action_group.ops_email.id
  }
}

Conclusion

Monitoring blob storage is not a one-time setup task—it's an evolving discipline that matures alongside your application. Start by enabling platform metrics and diagnostic logs, then progressively layer on targeted alerts for throttling, latency, and egress anomalies. Build dashboards that give both operators and developers a shared view of storage health, and embed your monitoring configuration in infrastructure-as-code to ensure consistency across environments. The time invested in tuning alerts, refining KQL queries, and automating remediation pays dividends in reduced downtime, controlled costs, and a storage infrastructure that scales predictably with your business. As blob storage continues to absorb more of your data estate—from transactional workloads to analytical lakehouses—the monitoring foundation you build today will be the lens through which you maintain visibility, security, and performance at scale.

🚀 Need a reliable AI agent for your project?

Deploy Hermes Agent in 10 minutes. Managed hosting, zero DevOps.

Get Started — $23.99/mo
← Back to all articles