Introduction to App Service Monitoring
What is App Service Monitoring?
App Service monitoring refers to the continuous collection, analysis, and visualization of performance data from your web applications and the underlying App Service infrastructure. In platforms like Azure App Service, this includes a rich set of built-in metrics covering CPU usage, memory consumption, HTTP traffic patterns, response times, and error rates. Monitoring combines three essential pillars: metrics (the raw numerical data points), alarms (automated alerts triggered when thresholds are breached), and dashboards (visual canvases that bring metrics together for real-time observation).
Modern App Service platforms surface metrics through multiple channels — the Azure Portal, Azure Monitor, Application Insights, REST APIs, and CLI tools. Each metric is a time-series data point with properties like timestamp, value, and dimension (for example, breaking down HTTP 500 errors by instance or by endpoint). Understanding this data flow is the foundation of operational excellence for any production web workload.
Why Monitoring Matters
Without monitoring, you are flying blind. A seemingly healthy application can silently degrade under load, accumulate memory leaks, or start serving errors to a subset of users without any visible symptom until customer complaints arrive. Effective monitoring gives you:
- Early detection — catch CPU spikes or memory pressure before the app crashes
- Capacity planning — understand traffic patterns to scale out (or in) at the right moment
- Cost optimization — identify over-provisioned instances wasting compute resources
- Root cause analysis — correlate deployment events with metric anomalies to pinpoint which release introduced a regression
- SLA compliance — prove uptime and performance targets to stakeholders with hard data
The cost of not monitoring is measured in downtime hours, lost revenue, and engineering time spent firefighting issues that could have been prevented. A well-instrumented App Service pays for itself many times over.
Understanding Metrics in App Service
Key Metrics to Monitor
App Service exposes dozens of metrics. Here are the ones you absolutely need to track, grouped by what they tell you:
Infrastructure-level metrics (App Service Plan):
CPU Percentage— average CPU usage across all instances. Sustained values above 80% indicate you need to scale up or out.Memory Percentage— average memory usage. Watch for upward trends that never return to baseline — a classic memory leak signature.Http Queue Length— number of HTTP requests queued waiting to be processed. Non-zero values mean your app is saturated.Disk Queue Length— pending disk I/O operations. High values point to slow storage or inefficient file operations.
Application-level metrics (App Service instance):
Http5xx— server-side errors. Any non-zero value deserves immediate investigation.Http4xx— client-side errors (404, 401, etc.). Useful for detecting broken links, expired tokens, or misconfigured auth.Requests— total request count. The foundation for calculating throughput (requests per second).Average Response Time— end-to-end latency in seconds. Segment by endpoint to find slow pages.App Connections— number of bound sockets. A sudden spike can indicate connection leaks or a DDoS pattern.Bytes Received / Bytes Sent— network throughput. Useful for bandwidth planning and detecting unusual data exfiltration patterns.
Application Insights metrics (if enabled):
Request Duration— server response time in milliseconds with percentile breakdowns (P50, P95, P99)Dependency Duration— time spent calling databases, external APIs, or storageFailure Rate— percentage of failed requests, segmented by operation nameActive Users / Sessions— real-user engagement metrics
Accessing Metrics Programmatically
The Azure Portal provides a rich metrics explorer, but automation demands programmatic access. Here are the primary methods:
Azure CLI — Fetching CPU metrics for the last hour:
#!/bin/bash
# Get the resource ID for your App Service Plan
APP_PLAN_ID=$(az appservice plan show \
--resource-group myResourceGroup \
--name myAppServicePlan \
--query id -o tsv)
# Query CPU percentage metric with 5-minute grain
az monitor metrics list \
--resource "$APP_PLAN_ID" \
--metric "CpuPercentage" \
--aggregation "Average" \
--interval 5m \
--start-time "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
--end-time "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--output table
Azure PowerShell — Multi-metric query with dimension filtering:
# Connect to Azure account
Connect-AzAccount
# Define parameters
$resourceGroup = "myResourceGroup"
$appServiceName = "myWebApp"
$resourceId = "/subscriptions/xxxx/resourceGroups/$resourceGroup/providers/Microsoft.Web/sites/$appServiceName"
# Fetch multiple metrics in one call
$metrics = Get-AzMetric `
-ResourceId $resourceId `
-MetricName @("Http5xx", "Requests", "AverageResponseTime") `
-TimeGrain "PT5M" `
-StartTime (Get-Date).AddHours(-6) `
-AggregationType "Total","Average"
# Display results
foreach ($metric in $metrics.Data) {
Write-Host "Metric: $($metric.TimeStamp) - $(($metric | Select-Object).Total)"
}
Azure Monitor REST API — Generic HTTP query:
# Authenticate and get token (using service principal)
TOKEN=$(curl -s -X POST \
-d "client_id=$CLIENT_ID&client_secret=$CLIENT_SECRET&resource=https://management.azure.com&grant_type=client_credentials" \
https://login.microsoftonline.com/$TENANT_ID/oauth2/token \
| jq -r .access_token)
# Query metrics for the last 30 minutes
curl -s -H "Authorization: Bearer $TOKEN" \
"https://management.azure.com/subscriptions/$SUB_ID/resourceGroups/$RG/providers/Microsoft.Web/sites/$APP_NAME/providers/microsoft.insights/metrics?api-version=2018-01-01&metricnames=Http5xx,Requests&aggregation=Total,Average×pan=2024-01-01T00:00:00Z/2024-01-01T00:30:00Z&interval=PT5M" \
| jq '.value[] | {name: .name.value, data: .timeseries[0].data}'
Application Insights KQL — Querying telemetry directly:
// P95 response time by operation over the last hour
requests
| where timestamp > ago(1h)
| summarize
P50 = percentile(duration, 50),
P95 = percentile(duration, 95),
P99 = percentile(duration, 99),
RequestCount = count()
by operation_Name
| order by P95 desc
// Dependency failures grouped by target service
dependencies
| where timestamp > ago(24h)
| where success == false
| summarize FailureCount = count(),
AvgDuration = avg(duration)
by target, dependencyType
| order by FailureCount desc
Setting Up Alarms (Alerts)
Alert Types and Rules
Alarms transform passive metrics into active notifications. Azure Monitor supports several alert types relevant to App Service:
- Metric threshold alerts — fire when a metric crosses a static value (e.g., CPU > 90% for 5 minutes). The simplest and most common type.
- Dynamic threshold alerts — use machine learning to establish a baseline and alert on deviations. Excellent for metrics with seasonal patterns like daytime traffic spikes.
- Log search alerts — run a KQL query on a schedule and alert based on the result count. Perfect for Application Insights data like "more than 10 exceptions in 5 minutes."
- Activity log alerts — trigger on management operations like scaling, restarting, or deleting an App Service. Critical for security and change tracking.
- Smart Detection alerts — built into Application Insights; automatically detect unusual patterns in failure rates and performance without any configuration.
Each alert rule needs an action group defining what happens when the alert fires: email, SMS, push notification, webhook call to PagerDuty or Opsgenie, or triggering an Azure Function for auto-remediation.
Creating Alerts with Infrastructure as Code
ARM Template — CPU alert with email notification:
{
"type": "Microsoft.Insights/metricAlerts",
"apiVersion": "2018-03-01",
"name": "high-cpu-alert-appservice",
"location": "global",
"properties": {
"description": "Alert when CPU exceeds 85% for 10 minutes",
"severity": 2,
"enabled": true,
"scopes": [
"[resourceId('Microsoft.Web/serverFarms', 'myAppServicePlan')]"
],
"evaluationFrequency": "PT5M",
"windowSize": "PT10M",
"criteria": {
"allOf": [
{
"metricName": "CpuPercentage",
"metricNamespace": "Microsoft.Web/serverFarms",
"operator": "GreaterThan",
"threshold": 85,
"timeAggregation": "Average",
"dimensions": []
}
],
"odata.type": "Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria"
},
"actions": [
{
"actionGroupId": "[resourceId('Microsoft.Insights/actionGroups', 'ops-team-notifications')]",
"webHookProperties": {}
}
]
}
}
Azure CLI — Creating a dynamic threshold alert:
# Create a dynamic threshold alert for HTTP 5xx errors
az monitor metrics alert create \
--resource-group "myResourceGroup" \
--name "dynamic-http5xx-alert" \
--scopes "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/myWebApp" \
--metric "Http5xx" \
--aggregation "Total" \
--operator "GreaterThan" \
--dynamic-sensitivity "High" \
--evaluation-frequency "5m" \
--window-size "15m" \
--severity 3 \
--action-group "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.Insights/actionGroups/ops-team" \
--description "Dynamic alert on HTTP 5xx spikes"
Log search alert — KQL-based alert for exception bursts:
# Create a log search alert rule using Application Insights data
az monitor scheduled-query create \
--resource-group "myResourceGroup" \
--name "exception-burst-alert" \
--scopes "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.Insights/components/myAppInsights" \
--description "Alert when exception count exceeds 50 in 5 minutes" \
--query 'exceptions | where timestamp > ago(5m) | summarize count() | where count_ > 50' \
--evaluation-frequency "5m" \
--window-size "5m" \
--severity 2 \
--action-groups "/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.Insights/actionGroups/ops-team"
Bicep — Modern declarative alert rule:
resource cpuAlert 'Microsoft.Insights/metricAlerts@2018-03-01' = {
name: 'high-memory-alert'
location: 'global'
properties: {
description: 'Memory percentage exceeds 90%'
severity: 2
enabled: true
scopes: [appServicePlan.id]
evaluationFrequency: 'PT5M'
windowSize: 'PT15M'
criteria: {
'allOf': [
{
metricName: 'MemoryPercentage'
metricNamespace: 'Microsoft.Web/serverFarms'
operator: 'GreaterThan'
threshold: 90
timeAggregation: 'Average'
}
]
'odata.type': 'Microsoft.Azure.Monitor.SingleResourceMultipleMetricCriteria'
}
actions: [
{
actionGroupId: actionGroup.id
}
]
}
}
Action Groups — Defining What Happens When Alerts Fire
An action group is a reusable collection of notification channels. Here's how to create one with email, webhook, and Azure Function integration:
az monitor action-group create \
--resource-group "myResourceGroup" \
--name "ops-team-critical" \
--short-name "ops" \
--action email ops-email ops-alerts@company.com \
--action webhook pagerduty \
"https://events.pagerduty.com/integration/xxx/enqueue" \
"pd_service=prod-app-service" \
--action azurefunction auto-scale-function \
"/subscriptions/xxx/resourceGroups/myResourceGroup/providers/Microsoft.Web/sites/AutoRemediation/functions/AutoScaleOut"
Building Effective Dashboards
Dashboard Design Principles
A dashboard is not a data dump — it's a curated visual narrative of your application's health. Follow these principles:
- Hierarchical layout — put the most critical signals (error rates, availability) at the top-left where eyes land first. Place drill-down charts below and to the right.
- Time-range consistency — all tiles on a dashboard should share the same time range (e.g., last 24 hours) to enable correlation. Avoid mixing 7-day and 1-hour charts on the same canvas.
- Use pinning aggressively — from the Metrics Explorer, pin specific chart configurations directly to dashboards. Each tile retains its exact metric, aggregation, and time window.
- Group by audience — create separate dashboards for developers (detailed, with Application Insights data), operations (infrastructure health, scaling), and executives (SLA summary, uptime percentages).
- Add annotations — use markdown tiles to document what each section means and link to runbooks for common alert responses.
Creating Dashboards Programmatically
Azure Dashboard JSON — Full dashboard definition:
{
"location": "australiaeast",
"tags": {
"environment": "production",
"team": "platform-engineering"
},
"properties": {
"lenses": [
{
"order": 0,
"parts": [
{
"position": { "x": 0, "y": 0, "rowSpan": 2, "colSpan": 6 },
"metadata": {
"inputs": [],
"type": "Extension/Microsoft_Azure_Monitoring/Monitoring_Metrics_Chart",
"settings": {
"content": {
"resourceId": "/subscriptions/xxx/resourceGroups/prod-rg/providers/Microsoft.Web/sites/prod-api",
"metrics": [
{
"name": "Http5xx",
"aggregation": "Sum",
"namespace": "Microsoft.Web/sites"
}
],
"title": "HTTP 5xx Errors - Last 24 Hours",
"timeRange": { "type": "Relative", "value": "P1D" },
"chartType": "Area"
}
}
}
},
{
"position": { "x": 6, "y": 0, "rowSpan": 2, "colSpan": 6 },
"metadata": {
"inputs": [],
"type": "Extension/Microsoft_Azure_Monitoring/Monitoring_Metrics_Chart",
"settings": {
"content": {
"resourceId": "/subscriptions/xxx/resourceGroups/prod-rg/providers/Microsoft.Web/serverFarms/prod-plan",
"metrics": [
{
"name": "CpuPercentage",
"aggregation": "Average",
"namespace": "Microsoft.Web/serverFarms"
},
{
"name": "MemoryPercentage",
"aggregation": "Average",
"namespace": "Microsoft.Web/serverFarms"
}
],
"title": "CPU & Memory - App Service Plan",
"timeRange": { "type": "Relative", "value": "P1D" },
"chartType": "Line"
}
}
}
},
{
"position": { "x": 0, "y": 2, "rowSpan": 3, "colSpan": 12 },
"metadata": {
"inputs": [],
"type": "Extension/Microsoft_Azure_Monitoring/AppInsights_QueryResultsTile",
"settings": {
"content": {
"query": "requests | where timestamp > ago(24h) | summarize avg(duration) by bin(timestamp, 1h) | order by timestamp asc | render timechart",
"title": "Average Response Time Trend (Hourly)",
"appInsightsResourceId": "/subscriptions/xxx/resourceGroups/prod-rg/providers/Microsoft.Insights/components/prod-appinsights"
}
}
}
},
{
"position": { "x": 0, "y": 5, "rowSpan": 2, "colSpan": 4 },
"metadata": {
"inputs": [],
"type": "Extension/Microsoft_Azure_Monitoring/Monitoring_Metrics_Chart",
"settings": {
"content": {
"resourceId": "/subscriptions/xxx/resourceGroups/prod-rg/providers/Microsoft.Web/sites/prod-api",
"metrics": [
{ "name": "Requests", "aggregation": "Sum", "namespace": "Microsoft.Web/sites" }
],
"title": "Request Volume",
"timeRange": { "type": "Relative", "value": "P7D" },
"chartType": "Bar"
}
}
}
},
{
"position": { "x": 4, "y": 5, "rowSpan": 2, "colSpan": 4 },
"metadata": {
"inputs": [],
"type": "Extension/Microsoft_Azure_Monitoring/Monitoring_Metrics_Chart",
"settings": {
"content": {
"resourceId": "/subscriptions/xxx/resourceGroups/prod-rg/providers/Microsoft.Web/sites/prod-api",
"metrics": [
{ "name": "AverageResponseTime", "aggregation": "Average", "namespace": "Microsoft.Web/sites" }
],
"title": "Response Time P50",
"timeRange": { "type": "Relative", "value": "P7D" }
}
}
}
},
{
"position": { "x": 8, "y": 5, "rowSpan": 2, "colSpan": 4 },
"metadata": {
"inputs": [],
"type": "Extension/Microsoft_Azure_Monitoring/Monitoring_AlertsList",
"settings": {
"content": {
"title": "Active Alerts",
"filter": {
"resourceGroup": "prod-rg",
"severity": [0, 1, 2]
}
}
}
}
}
]
}
]
}
}
Azure CLI — Creating a dashboard from JSON file:
az dashboard create \
--resource-group "myResourceGroup" \
--name "production-app-dashboard" \
--location "australiaeast" \
--definition @dashboard-definition.json \
--tags "environment=production team=platform"
Azure PowerShell — Scripted dashboard creation with dynamic resource IDs:
# Dynamically resolve resource IDs and build dashboard
$appServiceId = (Get-AzWebApp -ResourceGroupName "prod-rg" -Name "prod-api").Id
$planId = (Get-AzAppServicePlan -ResourceGroupName "prod-rg" -Name "prod-plan").Id
$appInsightsId = (Get-AzApplicationInsights -ResourceGroupName "prod-rg" -Name "prod-appinsights").Id
$dashboard = @{
location = "australiaeast"
properties = @{
lenses = @(
@{
order = 0
parts = @(
@{
position = @{ x = 0; y = 0; rowSpan = 2; colSpan = 6 }
metadata = @{
type = "Extension/Microsoft_Azure_Monitoring/Monitoring_Metrics_Chart"
settings = @{
content = @{
resourceId = $appServiceId
metrics = @(
@{ name = "Http5xx"; aggregation = "Sum"; namespace = "Microsoft.Web/sites" }
)
title = "HTTP 5xx Errors"
timeRange = @{ type = "Relative"; value = "P1D" }
}
}
}
}
)
}
)
}
}
$dashboardJson = $dashboard | ConvertTo-Json -Depth 10
New-AzDashboard -ResourceGroupName "prod-rg" -Name "production-app-dashboard" -Definition $dashboardJson
Pin Visualizations from Metrics Explorer
The fastest way to build dashboards is pinning live charts directly from the Azure Portal's Metrics Explorer, then exporting the dashboard JSON for version control. Once pinned, you can retrieve the dashboard definition programmatically:
# Export existing dashboard as JSON for source control
az dashboard show \
--resource-group "myResourceGroup" \
--name "production-app-dashboard" \
--output json > dashboard-export.json
# You can now commit this to Git and redeploy it to other environments
Best Practices for App Service Monitoring
1. Establish a Baseline Before Setting Alerts
Deploy your application, let it run under normal load for at least 48 hours, and collect metric data before configuring threshold alerts. Without a baseline, you risk setting thresholds too low (alert fatigue from false positives) or too high (missing real incidents). Use dynamic threshold alerts during this learning period — they adapt automatically.
2. Implement the "Three Pillars" of Alerting
Every production App Service should have alerts covering availability (HTTP 5xx rate > threshold), performance (response time P95 > SLA target), and capacity (CPU or memory > 85%). These three pillars catch the vast majority of production incidents before they escalate.
3. Use Multi-Stage Alert Escalation
Create at least two severity levels: Warning (severity 3-4) for conditions that need attention within hours, and Critical (severity 0-1) for conditions requiring immediate response. Route warnings to email or Slack during business hours; route critical alerts to PagerDuty with on-call rotation 24/7. A sample alert matrix:
# Warning level - CPU > 70% for 15 minutes during business hours
az monitor metrics alert create \
--name "cpu-warning" --severity 3 \
--threshold 70 --window-size "15m" \
--action-group "slack-notification-channel"
# Critical level - CPU > 90% for 5 minutes at any time
az monitor metrics alert create \
--name "cpu-critical" --severity 1 \
--threshold 90 --window-size "5m" \
--action-group "pagerduty-oncall-rotation"
4. Correlate Deployments with Metrics
Tag every deployment with a marker in Application Insights or annotate your dashboards with deployment events. When a metric anomaly appears, the first question is always "what changed?" Having deployment markers visible on the same timeline as metrics cuts root cause investigation from hours to minutes:
// Application Insights - track deployments as custom events
// Send this from your CI/CD pipeline after each deployment
customEvents
| where name == "Deployment"
| project timestamp,
DeploymentVersion = customDimensions.Version,
DeploymentUser = customDimensions.User,
Environment = customDimensions.Environment
| order by timestamp desc
// Query to correlate deployments with error spikes
let deployments = customEvents
| where name == "Deployment"
| project DeploymentTime = timestamp, Version = customDimensions.Version;
requests
| where timestamp > ago(24h)
| summarize ErrorCount = countif(resultCode startswith "5") by bin(timestamp, 5m)
| extend Deployment = tostring(deployments | where DeploymentTime between (bin_start(timestamp, 5m) .. bin_end(timestamp, 5m)) | project Version)
| order by timestamp asc
5. Monitor the Monitor
Alert rules themselves can fail — action groups may have expired webhook URLs, or metric ingestion might be throttled. Create a "meta-alert" that fires when expected heartbeat metrics are missing, indicating a monitoring pipeline failure:
// Detect missing heartbeat — no requests logged in 10 minutes
// This catches App Service outages AND monitoring failures
let heartbeat = requests
| where timestamp > ago(10m)
| count;
let threshold = 1;
heartbeat | where count_ < threshold
| project "No requests received in last 10 minutes — possible outage or monitoring failure"
6. Right-Size Your Metric Granularity
Balance granularity against cost and noise. For production workloads, 5-minute intervals provide sufficient resolution for alerting while keeping data volume manageable. Use 1-minute granularity only for critical, high-throughput services where rapid detection matters. For long-term trend analysis (capacity planning), aggregate to 1-hour or 1-day buckets and store in a separate analytics database to avoid cluttering your operational dashboards.
7. Build Runbook-Linked Dashboards
Every dashboard tile that represents a potential incident should link to a corresponding runbook. Add markdown tiles with direct links to your incident response procedures, playbooks, and relevant Azure Monitor workbooks. A well-built operations dashboard answers not just "what is happening?" but also "what do I do about it?"
8. Automate Remediation Where Safe
For well-understood conditions, wire alerts directly to Azure Functions or Logic Apps that perform automatic remediation — scaling out when queue length grows, restarting a single instance when memory exceeds threshold, or clearing a cache when disk space runs low. Always pair automated actions with notifications so the on-call team is informed:
# Azure Function triggered by alert webhook for auto-scale
# function.json binding
{
"bindings": [
{
"authLevel": "function",
"type": "httpTrigger",
"direction": "in",
"name": "req",
"methods": ["post"],
"route": "auto-scale-out"
},
{
"type": "http",
"direction": "out",
"name": "res"
}
]
}
# run.csx - Parse alert context and scale out
using System.Net;
using System.Text;
public static async Task Run(
HttpRequestMessage req,
ILogger log)
{
var content = await req.Content.ReadAsStringAsync();
dynamic alert = JsonConvert.DeserializeObject(content);
string metricName = alert.data.essentials.metricName;
double threshold = alert.data.essentials.monitoringThreshold;
log.LogInformation($"Alert received: {metricName} exceeded {threshold}");
// Call Azure Management API to scale out
// ... scale operation logic ...
return new HttpResponseMessage(HttpStatusCode.OK);
}
9. Version-Control Your Monitoring Configuration
Treat alert rules, action groups, and dashboard definitions as code. Store ARM templates or Bicep files in the same repository as your application code. Deploy monitoring configuration alongside each release. This ensures monitoring evolves with your application and prevents configuration drift between environments. A typical monitoring deployment pipeline:
# Deploy monitoring infrastructure via Azure DevOps pipeline
# Step 1: Deploy action groups
az deployment group create \
--resource-group "prod-rg" \
--template-file "monitoring/action-groups.bicep" \
--parameters environment=production
# Step 2: Deploy alert rules
az deployment group create \
--resource-group "prod-rg" \
--template-file "monitoring/alert-rules.bicep" \
--parameters @alert-params.json
# Step 3: Deploy dashboards
az deployment group create \
--resource-group "prod-rg" \
--template-file "monitoring/dashboards.bicep"
# Step 4: Validate all alerts are active
az monitor metrics alert list \
--resource-group "prod-rg" \
--query "[?enabled==true].name" -o tsv
10. Regularly Review and Prune
Schedule a quarterly review of all alert rules. Disable or adjust rules that have fired zero times (they may be misconfigured) or fired too frequently (causing alert fatigue). Archive dashboards that are no longer actively used. Monitoring configuration rots just like code — keep it maintained.
Conclusion
Monitoring your App Service through metrics, alarms, and dashboards transforms your application from a black box into a transparent, manageable system. The journey starts with understanding which metrics matter for your specific workload, then methodically building alert rules that wake you up before users notice a problem, and finally crafting dashboards that give your entire team a shared view of system health. The code examples in this tutorial — from Azure CLI queries to full ARM template deployments — give you the practical tools to implement a production-grade monitoring setup. Start with the three-pillar alerting foundation (availability, performance, capacity), version-control everything as infrastructure-as-code, and iterate based on what your real production data teaches you. A well-monitored App Service is the bedrock of reliable cloud operations.