Keeping an eye on Azure Storage Queue backlogs is essential for reliable systems and scale decisions. Most teams want per-queue visibility (not just account-level metrics), simple dashboards/alerts, and a repeatable deployment story. This blog documents a practical approach I use in this GitHub repo. A PowerShell Azure Function that emits per-queue message counts as custom metrics to Application Insights, plus Bicep and GitHub Actions to deploy the environment and seed test data.
We’ll cover the problem, the design, some tricky implementation details (Managed Identity auth, CloudQueue vs QueueClient data paths), and how to visualize the results. Everything shown lives in this repository so you can clone and run it end-to-end.
The problem and the constraints
Azure Monitor’s built-in QueueMessageCount for storage accounts is hourly and not split per queue. You can’t retrieve per-queue counts via Azure Monitor metrics.
Teams need per-queue counts at a 5 minute cadence to alert on spikes and monitor backlog trends.
We want secure auth (Managed Identity), no keys in code, and a minimal footprint.
Solution overview
We poll each queue in a storage account with Az.Storage and emit a custom metric per queue to Application Insights using the v2 ingestion endpoint. Workbooks and Metrics Explorer can then visualize and alert on these metrics by the QueueName dimension.
High level flow:
Timer-trigger function runs every 5 minutes.
Uses Managed Identity to authenticate to the storage account data plane.
Enumerates queues and queries an approximate visible message count per queue.
Sends a custom metric item per queue to Application Insights with dimensions StorageAccount and QueueName.
Prerequisites and configuration
RBAC and Managed Identity
The Function App uses a system-assigned managed identity. Assign one of these data-plane roles at the storage account:
Then authenticate with New-AzStorageContext -UseConnectedAccount. This avoids keys and works well in Functions.
App settings (environment variables)
The function expects these settings (set as Function App application settings or local environment variables):
Function implementation (PowerShell)
The function is functionApp/QueueMessageCount/run.ps1. It uses Managed Identity via New-AzStorageContext -UseConnectedAccount and supports both the legacy WindowsAzure.Storage path and the modern Azure.Storage.Queues path.
Key setup and auth:
$ctx = New-AzStorageContext -StorageAccountName $StorageAccountName -UseConnectedAccount -ErrorAction Stop
$queues = Get-AzStorageQueue -Context $ctx -ErrorAction Stop
Why -UseConnectedAccount matters
-UseConnectedAccount tells Az.Storage to use the Azure AD token from your current Az context (in Functions, the system-assigned managed identity from Connect-AzAccount -Identity) to authenticate to the Storage data plane. That has a few important implications:
In short: Managed Identity + no keys means use -UseConnectedAccount.
- The legacy
CloudQueue path often isn’t available under AAD; preference the QueueClient path when using MI.
Reading the data path:
$props = $qref.QueueClient.GetProperties()
$approx = $props.Value.ApproximateMessagesCount
if ($null -ne $approx) { $value = [int]$approx }
Sending a metric to Application Insights (v2 track endpoint):
$endpoint = $IngestionEndpoint.TrimEnd('/') + '/v2/track'
$env = @{
name = 'Microsoft.ApplicationInsights.Metric'
time = (Get-Date).ToString('o')
iKey = $ikey
data = @{
baseType = 'MetricData'
baseData = @{
ver = 2
metrics = @( @{ name = 'QueueMessageCount'; value = [double]$value } )
properties = @{ StorageAccount = $StorageAccountName; QueueName = $queueName }
}
}
}
Invoke-RestMethod -Method Post -Uri $endpoint -ContentType 'application/json' -Body ($env | ConvertTo-Json -Depth 10)
How the custom metric works:
Connection string notes and validating ingestion:

Why per-queue via SDK and not Azure Monitor metrics?
Azure’s built-in metric QueueMessageCount for Microsoft.Storage/storageAccounts/queueServices is sampled hourly and has no per-queue dimension. That’s great for account-level trends, but not for operational backlogs per queue. Reading the approximate visible count with the SDK provides timely, per-queue values suitable for dashboards and alerts.
Infrastructure as Code (Bicep)
The Bicep file infra/main.bicep provisions:
Storage account (Standard_LRS)
Queue service and six randomly named queues
App Insights instance
Consumption Function App (PowerShell) with a system-assigned managed identity
A file share for function content settings
Notable settings injected into the Function App:
siteConfig: {
appSettings: [
{ name: 'APPLICATIONINSIGHTS_CONNECTION_STRING', value: appInsights.properties.ConnectionString }
{ name: 'STORAGE_ACCOUNT_NAME', value: st.name }
{ name: 'STORAGE_RESOURCE_GROUP', value: resourceGroup().name }
{ name: 'AZURE_SUBSCRIPTION_ID', value: subscription().subscriptionId }
]
}
Outputs include the queue names and the storage/account info, which our scripts consume.
CI/CD with GitHub Actions
Two workflows are included:
These workflows take optional inputs for names, otherwise they auto-generate compliant names.
Seeding data and repeatable tests (scripts)
Two helper scripts in scripts/ create sample queues and populate messages using Azure CLI:
create-queues.ps1 reads deployment outputs and creates queues:
$out = az deployment group show --resource-group $ResourceGroup --name $DeploymentName --query properties.outputs -o json | ConvertFrom-Json
$storageName = $out.storageAccount.value
$queues = $out.queueNames.value
$conn = az storage account show-connection-string --resource-group $ResourceGroup --name $storageName -o tsv
foreach ($q in $queues) { az storage queue create --name $q --connection-string $conn }
populate-queues.ps1 fills each queue with a random number of messages:
$out = az deployment group show --resource-group $ResourceGroup --name $DeploymentName --query properties.outputs -o json | ConvertFrom-Json
$storageName = $out.storageAccount.value
$queues = $out.queueNames.value
$conn = az storage account show-connection-string --resource-group $ResourceGroup --name $storageName -o tsv
foreach ($q in $queues) {
$count = Get-Random -Minimum $MinMessages -Maximum ($MaxMessages + 1)
for ($i = 0; $i -lt $count; $i++) { az storage message put --queue-name $q --content "msg-$([random]::new().Next(100000,999999))" --connection-string $conn }
}
These are used automatically in the GitHub workflow after deployment to generate a non-zero baseline for monitoring.
Visualizing and alerting
You can use either Metrics Explorer or Workbooks.
customMetrics
| where name == "QueueMessageCount"
| summarize avg(value) by tostring(customDimensions.QueueName), bin(timestamp, 5m)
| order by timestamp desc
Create a metric alert on the custom metric (dimension: QueueName) or a Log Analytics alert using a scheduled query. You can use the same query to visual the data in Workbooks

Troubleshooting: why would counts be 0?
Missing data-plane role: assign Storage Queue Data Reader/Contributor to the Function’s managed identity.
Using account keys but wrong context: prefer -UseConnectedAccount with MI.
Wrong data path: on AAD auth, QueueClient.GetProperties().Value.ApproximateMessagesCount is the reliable path; CloudQueue may not be present.
Immediate staleness: approximate counts lag slightly; verify via a quick Peek if in doubt.
Conclusion
Per-queue message counts are not available via Azure Monitor metrics, but they’re straightforward to gather the metrics with Az.Storage and publish as custom metrics in Application Insights. With Managed Identities, Bicep, and GitHub Actions, you can deploy the whole pipeline, seed data, and put dashboards/alerts in front of your team in an hour.
The code here is ready to use with a small footprint and clear extension points (retry/backoff, filtering queues, sampling). Clone the repo, deploy, and start monitoring. 🚀
References