Reading time: 5 minutes, 8 seconds
Azure Application Gateway is more than just a load balancer — it’s a full Layer 7 application delivery controller with WAF, SSL offload, URL-based routing, multi-site hosting, and deep observability. This guide covers everything an Azure Architect or Engineer needs to know.
1. Architecture Overview
Application Gateway operates at Layer 7 (HTTP/HTTPS), handling traffic inspection, routing, and TLS termination before requests reach your application.

Key Components
- Frontend IP — Public and/or private IP. Standard v2 supports both simultaneously.
- Listeners — Define protocol (HTTP/HTTPS), port, hostname. Basic or Multi-site.
- WAF Policy — OWASP 3.2 ruleset, custom rules, bot protection, geo-filtering.
- Routing Rules — Associate listener with backend pool + HTTP settings. Basic or Path-based.
- Backend Pools — VMs, VMSS, App Services, AKS pods, on-premises IPs, FQDNs.
- HTTP Settings — Port, protocol, timeouts, cookie affinity, connection draining.
- Health Probes — Custom or default probes for backend health checks.
SKU Comparison
| Feature | Standard v2 | WAF v2 |
|---|---|---|
| Layer 7 LB | ✅ | ✅ |
| Autoscaling | ✅ | ✅ |
| Zone redundancy | ✅ | ✅ |
| WAF (OWASP) | ❌ | ✅ |
| Custom WAF rules | ❌ | ✅ |
| Bot protection | ❌ | ✅ |
💡 Architect tip: Always deploy WAF v2 in production. Use Detection mode first to baseline traffic, then switch to Prevention mode.
resource appGateway 'Microsoft.Network/applicationGateways@2023-04-01' = { name: 'agw-prod' zones: ['1', '2', '3'] properties: { sku: { name: 'WAF_v2', tier: 'WAF_v2' } autoscaleConfiguration: { minCapacity: 2, maxCapacity: 10 } }}
2. Routing Options
2.1 Basic Routing
All traffic from a listener goes to a single backend pool. Use for single-application scenarios.
2.2 URL Path-Based Routing
Route to different backend pools based on URL path — the most common pattern for microservices.

urlPathMaps: [{ name: 'url-path-map' properties: { defaultBackendAddressPool: { id: webPool.id } pathRules: [ { name: 'api-rule', paths: ['/api/v1/*', '/api/v2/*'], backendPool: apiPool }, { name: 'static-rule', paths: ['/images/*', '/static/*'], backendPool: cdnPool } ] }}]
⚠️ Gotcha: Path rules are evaluated in order — first match wins. Put specific paths before wildcards. The default rule catches everything unmatched.
2.3 Multi-Site (Hostname-Based) Routing
Host multiple applications on a single gateway with one public IP. Each listener binds to a specific hostname with its own TLS cert and backend pool.

2.4 Wildcard Listeners
Listeners support wildcard hostnames (e.g., *.contoso.com) to catch all subdomains with a single listener.
2.5 HTTP Header and URL Rewrite Rules
Modify request/response headers and URLs without touching backend code. Common uses: strip path prefixes, inject security headers, remove sensitive response headers like Server and X-Powered-By.
rewriteRules: [{ name: 'strip-api-prefix-and-add-security-headers' ruleSequence: 100 conditions: [{ variable: 'var_uri_path', pattern: '/api/(.*)' }] actionSet: { urlConfiguration: { modifiedPath: '/{var_uri_path_1}' } responseHeaderConfigurations: [ { headerName: 'Server', headerValue: '' }, { headerName: 'Strict-Transport-Security', headerValue: 'max-age=31536000; includeSubDomains' }, { headerName: 'X-Content-Type-Options', headerValue: 'nosniff' }, { headerName: 'X-Frame-Options', headerValue: 'DENY' } ] }}]
2.6 Redirects
Application Gateway handles HTTP→HTTPS redirects, path redirects, and external URL redirects natively — no backend logic needed.
💡 Trick: Enforce HTTPS at the gateway level to offload redirect logic from every backend service.
2.7 Connection Draining
Enable connection draining for graceful backend removal during rolling deployments. The gateway stops sending new requests to a deregistering backend while allowing existing connections to complete.
backendHttpSettings: { connectionDraining: { enabled: true, drainTimeoutInSec: 60 }}
3. CORS Policies — Scenarios and Examples
Application Gateway implements CORS via rewrite rules that inject the correct response headers centrally, rather than managing CORS on every backend.
Key CORS Headers
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin | Which origins are allowed |
Access-Control-Allow-Methods | Allowed HTTP methods |
Access-Control-Allow-Headers | Allowed request headers |
Access-Control-Allow-Credentials | Whether cookies/auth are allowed |
Access-Control-Max-Age | How long preflight results are cached |
Access-Control-Expose-Headers | Headers exposed to JavaScript |
Scenario 1: SPA Calling an API (Same Gateway)
React SPA at https://app.contoso.com calling REST API at https://api.contoso.com — both behind the same Application Gateway:
rewriteRules: [{ name: 'cors-spa-to-api' conditions: [{ variable: 'http_req_Origin', pattern: 'https://app\.contoso\.com' }] actionSet: { responseHeaderConfigurations: [ { headerName: 'Access-Control-Allow-Origin', headerValue: 'https://app.contoso.com' }, { headerName: 'Access-Control-Allow-Methods', headerValue: 'GET, POST, PUT, DELETE, OPTIONS' }, { headerName: 'Access-Control-Allow-Headers', headerValue: 'Authorization, Content-Type, X-Request-ID' }, { headerName: 'Access-Control-Allow-Credentials', headerValue: 'true' }, { headerName: 'Access-Control-Max-Age', headerValue: '3600' } ] }}]
Scenario 2: Handling OPTIONS Preflight
Route OPTIONS requests to a dedicated Azure Function returning 200 + CORS headers immediately, without hitting the main backend:
// Azure Function — OPTIONS preflight respondermodule.exports = async function (context, req) { context.res = { status: 200, headers: { 'Access-Control-Allow-Origin': req.headers['origin'] || '*', 'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS', 'Access-Control-Allow-Headers': 'Authorization, Content-Type', 'Access-Control-Max-Age': '86400' }, body: '' };};
Scenario 3: Multi-Origin Allowlist (dev/staging/prod)
conditions: [{ variable: 'http_req_Origin', pattern: 'https://(app|staging|dev)\.contoso\.com'}]actionSet: { responseHeaderConfigurations: [{ headerName: 'Access-Control-Allow-Origin', headerValue: '{http_req_Origin}' // Echo back the validated origin }]}
Scenario 4: Azure AD / OAuth2 (MSAL.js)
When using MSAL.js, ensure:
Access-Control-Allow-Credentials: true— required for session cookiesAccess-Control-Expose-Headers: WWW-Authenticate— required for 401 challenge handlingAccess-Control-Allow-Headersmust includeAuthorization
⚠️ Security: Never combine
Access-Control-Allow-Origin: *withAccess-Control-Allow-Credentials: true— browsers will block it. Always specify the exact origin when credentials are involved.
4. Certificate Management

4.1 Frontend Certificates — Three Approaches
| Method | Pros | Cons |
|---|---|---|
| Key Vault (recommended) | Auto-rotation, centralised, no PFX handling | Requires Managed Identity setup |
| PFX upload | Simple, no dependencies | Manual rotation, cert stored in gateway config |
| Let’s Encrypt via AGIC | Free, automated | AKS only, requires cert-manager |
// Step 1: Assign Key Vault Secrets User role to AGW Managed Identity// Step 2: Reference cert WITHOUT version for auto-rotationsslCertificates: [{ name: 'contoso-cert' properties: { // No version in the URI = picks up latest cert automatically (within 4h) keyVaultSecretId: 'https://kv-prod.vault.azure.net/secrets/contoso-tls' }}]
💡 Trick: Omit the version from the Key Vault secret ID. When the cert is rotated in Key Vault, Application Gateway automatically picks up the new version within 4 hours — zero manual steps.
4.2 Backend Certificates (End-to-End TLS)
- Public trusted CA: Enable “Use well-known CA certificate” in HTTP Settings — no upload needed.
- Internal/self-signed CA: Upload the root certificate (DER/CER format) as a Trusted Root Certificate in HTTP Settings.
4.3 TLS Policy — Enforce Strong Ciphers
sslPolicy: { policyType: 'Custom' minProtocolVersion: 'TLSv1_2' // Never allow TLS 1.0 or 1.1 cipherSuites: [ 'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384', 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256' ]}
4.4 Certificate Expiry Monitoring (KQL)
AzureDiagnostics| where Category == "ApplicationGatewayAccessLog"| where sslClientCertificateIssuer_s != ""| summarize by sslClientCertificateIssuer_s, bin(TimeGenerated, 1d)
5. Monitoring, Debugging and Alerts

5.1 Enable All Three Diagnostic Log Categories
- ApplicationGatewayAccessLog — Every request: client IP, URI, response code, latency, backend pool, backend response time.
- ApplicationGatewayPerformanceLog — Throughput, connection counts, healthy/unhealthy host counts per instance.
- ApplicationGatewayFirewallLog — WAF rule matches, blocked requests, anomaly scores (WAF v2 only).
5.2 Key Metrics and Alert Thresholds
| Metric | Alert Threshold | What it means |
|---|---|---|
UnhealthyHostCount | > 0 | Any backend failing health probe |
ResponseStatus (5xx) | > 1% of requests | Backend errors |
BackendLastByteResponseTime | > your SLO | Backend slowness |
CapacityUnits | > 75% of max | Approaching autoscale ceiling |
WAFTotalRequests (Blocked) | Sudden spike | Potential attack in progress |
5.3 Essential KQL Queries
Top backends by P95 response time:
AzureDiagnostics| where Category == "ApplicationGatewayAccessLog" and TimeGenerated > ago(1h)| extend backendLatency = todouble(backendResponseLatency_s)| summarize avg(backendLatency), percentile(backendLatency, 95) by backendAddress_s| order by percentile_backendLatency_95 desc
5xx errors by backend pool and URI:
AzureDiagnostics| where Category == "ApplicationGatewayAccessLog"| where httpStatus_d >= 500 and TimeGenerated > ago(1h)| summarize count() by requestUri_s, backendPoolName_s, httpStatus_d| order by count_ desc
WAF top blocked rules (last 24h):
AzureDiagnostics| where Category == "ApplicationGatewayFirewallLog"| where action_s == "Blocked" and TimeGenerated > ago(24h)| summarize count() by ruleId_s, ruleGroup_s, requestUri_s| order by count_ desc | take 20
P95 latency trend (renders as time chart):
AzureDiagnostics| where Category == "ApplicationGatewayAccessLog" and TimeGenerated > ago(3h)| summarize percentile(todouble(timeTaken_s), 95) by bin(TimeGenerated, 5m)| render timechart
5.4 Debugging Tips
- Backend health blade: Portal → Application Gateway → Backend Health. Real-time probe status per backend — fastest first stop for any routing issue.
- X-Forwarded-For: Ensure backends log this header. Without it, client IPs appear as the gateway’s subnet IP.
- 502 Bad Gateway: Almost always a health probe failure or TLS mismatch between gateway and backend. Check Backend Health first.
- Probe port mismatch: Probing on port 443 but sending traffic to port 80 = backends appear healthy but traffic silently fails.
- NSG mandatory rule: Inbound allow on ports 65200–65535 from
GatewayManagerservice tag is required for App Gateway v2 management plane. Without it the gateway won’t deploy or function.
6. Architect and Engineer Tricks
Custom Health Probes — Go Beyond Root Path
probes: [{ name: 'api-health-probe' properties: { protocol: 'Https', path: '/health/live' interval: 30, timeout: 10, unhealthyThreshold: 3 pickHostNameFromBackendHttpSettings: true match: { statusCodes: ['200-399'] } }}]
Application Gateway + Azure Front Door
For global architectures: Front Door handles global load balancing, CDN, DDoS, and geo-routing. Application Gateway handles regional URL routing and backend pools. Lock AGW to only accept Front Door traffic via WAF custom rule:
customRules: [{ name: 'AllowFrontDoorOnly', priority: 1, action: 'Block' matchConditions: [{ matchVariables: [{ variableName: 'RequestHeaders', selector: 'X-Azure-FDID' }] operator: 'Equal', negationCondition: true matchValues: ['your-front-door-resource-id'] }]}]
Private Link Backend Integration
Route traffic to backends exposed via Azure Private Link — keeping all traffic on the Microsoft backbone. Supports PaaS services (Azure SQL, Storage) and third-party services.
Common Mistakes to Avoid
- ❌ Single instance in production — Always set
minCapacity: 2. One instance = potential downtime during Azure platform updates. - ❌ No availability zone pinning — Always specify
zones: ['1','2','3']to avoid all capacity landing in a single AZ. - ❌ WAF in Detection mode permanently — Set a 30-day calendar reminder. Detection logs but never blocks — it’s not protection.
- ❌ Default 30s request timeout — Long-running APIs (reports, exports) will return 502. Tune timeout per backend pool HTTP settings.
- ❌ Missing NSG management rule — Ports 65200–65535 from
GatewayManagermust be allowed inbound or the gateway won’t function. - ❌ Cookie affinity as a permanent crutch — Prefer stateless backends + Redis Cache. Affinity prevents balanced load distribution and complicates blue/green deployments.
- ❌ Hardcoded cert versions in Key Vault references — Always omit the version suffix to get auto-rotation. A pinned version = manual cert rotation forever.
Summary
Azure Application Gateway is a foundational component in enterprise Azure architectures. When configured correctly, it provides a single secure ingress point with rich routing, WAF protection, automated TLS lifecycle, and deep observability.
- ✅ Deploy WAF v2 with zone redundancy and
minCapacity: 2 - ✅ Use Key Vault for cert management with auto-rotation (no version in secret ID)
- ✅ Use URL path routing to cleanly decompose monoliths and route microservices
- ✅ Handle CORS centrally via rewrite rules — not scattered across backends
- ✅ Enable all three diagnostic log categories and build KQL-based alerts
- ✅ Inject security headers centrally via rewrite rules
- ✅ Combine with Azure Front Door for global multi-region scenarios
Have questions or patterns to share? Drop them in the comments below.





