Azure App Service & Hosting
Deployment slots, auto-scaling, and the App Service plan β understanding what you're actually paying for.
Azure App Service is a fully managed PaaS for hosting web applications, REST APIs, and mobile backends. The platform handles OS patching, load balancing, and scaling, but the billing model, slot configuration, and Always On setting have non-obvious implications. Most production incidents trace back to three things: the wrong plan tier, connection strings that travel across a slot swap, or Kudu left open to the internet.
The App Service Plan is the compute resource: it defines the VM size, OS, region, and number of instances. All apps on the same plan share those instances β a B2 plan with 4 apps means all 4 apps compete for the same 2 vCores and 3.5 GB RAM. You pay for the plan whether or not apps are receiving requests.
Each slot (staging, preview, etc.) is a fully functional App Service app with its own hostname, configuration, and deployment target. When you swap slots, Azure warms up the new slot by sending a request to the site root, waits for a 200 response, then atomically swaps the routing rules. Traffic switches instantaneously; no DNS change required.
App settings and connection strings are swapped with the app by default. If your staging slot uses a staging database connection string and you forget to mark it slot-sticky, the staging connection string will become production's connection string after the swap. Mark settings as 'deployment slot setting' (slot-sticky) in the portal or via slotSetting: true in Bicep to pin them to the slot.
Without Always On enabled, Azure unloads your app's worker process after ~20 minutes of inactivity to reclaim resources. The next request will take 5β15 seconds while IIS/Kestrel restarts and your app initializes. Always On sends a periodic ping to keep the process alive. It is not available on Free or Shared tiers.
Auto-scale evaluates metric aggregations over a time window (e.g., average CPU over 5 minutes). After a scale action fires, the cooldown period prevents further scaling until metrics stabilize. Scale-out and scale-in cooldowns should be asymmetric: scale out quickly (5-min cooldown), scale in slowly (10β15 min cooldown) to avoid flapping during load spikes.
Key Concepts
F1 (Free, 60 CPU-min/day, no Always On, no slots) β B1 (Basic, 1 vCore, no slots, no auto-scale) β S1 (Standard, 5 slots, auto-scale) β P1v3 (Premium v3, 2 vCore, VNet, better cold-start). Never use F1 or B1 in production for latency-sensitive apps.
Named alternate environments (staging, canary) that each have their own URL, config, and deployment history. Swap moves traffic atomically. Available on Standard and above. The 'production' slot is the default.
App settings or connection strings marked as 'deployment slot setting' stay bound to the slot they're configured on β they do NOT swap with the app. Use this for slot-specific secrets like staging DB connection strings.
Keeps the app worker process alive by sending periodic ping requests. Without it, the process is recycled after idle timeout, causing cold starts on the first request. Unavailable on Free and Shared (D1) tiers.
App Service pings your /health endpoint every 2 minutes. If an instance fails 10 consecutive checks, it's removed from the load balancer and replaced. Requires at least 2 instances to be effective β one instance health check alerts only, doesn't replace.
The App Service management console at https://<appname>.scm.azurewebsites.net. Exposes a bash/PowerShell terminal, deployment logs, process explorer, and file system. It runs on the same compute and has no IP restriction by default β a significant attack surface.
Allows outbound calls from App Service into a private VNet (to reach databases, Redis, internal APIs). Only outbound β it does not make your app reachable from within the VNet. Regional VNet integration requires a dedicated subnet and Standard+ plan.
1// Bicep β App Service Plan + Web App with slots and auto-scale2resource plan 'Microsoft.Web/serverfarms@2023-01-01' = {3 name: 'plan-myapp-prod'4 location: resourceGroup().location5 sku: {6 name: 'P1v3' // Premium v3 β required for VNet integration and deployment slots7 tier: 'PremiumV3'8 capacity: 2 // Start with 2 instances; auto-scale will add up to 109 }10 properties: {11 reserved: true // Linux12 }13}1415resource app 'Microsoft.Web/sites@2023-01-01' = {16 name: 'app-myapp-prod'17 location: resourceGroup().location18 properties: {19 serverFarmId: plan.id20 siteConfig: {21 alwaysOn: true // REQUIRED on B1+ β not available on F1/D122 healthCheckPath: '/health' // Returns 200; unhealthy instances removed from LB23 vnetRouteAllEnabled: true // Route ALL outbound traffic through VNet (not just RFC1918)24 appSettings: [25 { name: 'WEBSITE_RUN_FROM_PACKAGE', value: '1' } // Read-only deployment, faster start26 { name: 'ASPNETCORE_ENVIRONMENT', value: 'Production' }27 ]28 connectionStrings: [29 {30 name: 'DefaultConnection'31 connectionString: '@Microsoft.KeyVault(SecretUri=https://mykv.vault.azure.net/secrets/db-conn)'32 type: 'SQLAzure'33 // slotSetting: false β DEFAULT β this conn string WILL be swapped with staging!34 // Set slotSetting: true to make it sticky (not swapped)35 }36 ]37 }38 virtualNetworkSubnetId: subnetId // VNet integration for outbound39 }40}4142// Staging deployment slot43resource stagingSlot 'Microsoft.Web/sites/slots@2023-01-01' = {44 name: 'staging'45 parent: app46 location: resourceGroup().location47 properties: {48 siteConfig: {49 alwaysOn: true50 appSettings: [51 { name: 'ASPNETCORE_ENVIRONMENT', value: 'Staging' }52 // Slot-sticky settings: slotSetting: true ensures these stay in staging after swap53 ]54 }55 }56}5758// Auto-scale rule β CPU-based scale out59resource autoScale 'Microsoft.Insights/autoscalesettings@2022-10-01' = {60 name: 'autoscale-myapp'61 location: resourceGroup().location62 properties: {63 targetResourceUri: plan.id64 enabled: true65 profiles: [{66 name: 'CPU-based'67 capacity: { minimum: '2', maximum: '10', default: '2' }68 rules: [69 {70 metricTrigger: {71 metricName: 'CpuPercentage'72 metricResourceUri: plan.id73 timeGrain: 'PT1M'74 statistic: 'Average'75 timeWindow: 'PT5M' // 5-min window β too short can cause flapping76 timeAggregation: 'Average'77 operator: 'GreaterThan'78 threshold: 7079 }80 scaleAction: { direction: 'Increase', type: 'ChangeCount', value: '1', cooldown: 'PT5M' }81 },82 {83 metricTrigger: {84 metricName: 'CpuPercentage'85 metricResourceUri: plan.id86 timeGrain: 'PT1M'87 statistic: 'Average'88 timeWindow: 'PT10M' // Scale-in window longer than scale-out to avoid flapping89 timeAggregation: 'Average'90 operator: 'LessThan'91 threshold: 3092 }93 scaleAction: { direction: 'Decrease', type: 'ChangeCount', value: '1', cooldown: 'PT10M' }94 }95 ]96 }]97 }98}
App Service abstracts away infrastructure management, but it does not abstract away configuration decisions. Slot swaps, sticky settings, plan tier constraints, and VNet integration all require explicit intent. A deployment pipeline that doesn't account for slot-sticky settings, or a cost-cutting move from S1 to F1, can create production incidents within minutes of the change taking effect.
Common Pitfalls
1Connection String Swap Took Down Production Database
A team deployed a new version to their staging slot with a staging SQL database. After load testing passed, they swapped staging to production. Within 2 minutes, all production users started receiving 'Login failed for user' SQL errors.
The staging slot had a connection string named 'DefaultConnection' pointing to the staging database. This connection string was NOT marked as slot-sticky. When the swap occurred, the staging connection string traveled with the app code into the production slot, pointing production traffic at the staging (empty) database.
Marked the 'DefaultConnection' setting as a deployment slot setting (slot-sticky) in both slots. The production slot now permanently holds its production connection string; staging permanently holds its staging string. After swap, the app code moves but the connection strings stay pinned to their respective slots.
Takeaway: Any setting that is environment-specific (DB connection strings, API keys pointing to environment-specific services) must be marked slot-sticky. Treat it as a mandatory deployment checklist item, not an optional configuration.
2Free Tier Cold Start Alarming Users Every Morning
A startup's dashboard app on the F1 (Free) tier had a support ticket every Monday morning: 'The app takes forever to load'. Investigation showed first-request latency of 12β18 seconds after the weekend.
F1 tier does not support Always On. After ~20 minutes without traffic, the IIS worker process was recycled. Monday morning's first user triggered a full cold start: ASP.NET Core initialization, DI container build, EF Core model compilation, and connection pool warmup all on one request. Free tier hardware compounded the issue.
Upgraded to B1 ($13/month) and enabled Always On. Cold starts dropped to <2 seconds (EF Core model cache still warms on first request). Added a /health endpoint that pre-warms the EF Core model on startup using a lightweight query. Set up an external uptime monitor (UptimeRobot) to ping /health every 5 minutes as a belt-and-suspenders keep-alive.
Takeaway: F1 and D1 (Shared) tiers are development-only. No Always On means cold starts for every user session that falls outside peak hours. The cost of B1 is less than one hour of senior developer time spent debugging user-reported slowness.
3Kudu Console Exposed Without IP Restriction
A security audit revealed that the app's Kudu console (https://appname.scm.azurewebsites.net) was accessible from the internet without any IP restriction. The audit team used it to list environment variables and read the application file system within 10 minutes of starting.
Kudu's SCM endpoint uses the same Azure AD authentication as the portal for management-plane access, but many teams accept any Azure login or use publish profile credentials. With no IP restriction, any attacker with valid credentials (or a leaked publish profile XML) has a terminal session with full filesystem and process access on the production web server.
Added an IP restriction in App Service networking to allow SCM access only from the team's VPN CIDR range (10.0.0.0/8) and the Azure DevOps agent subnet. Rotated all publish profile credentials. Migrated deployments to service principal + GitHub Actions, eliminating publish profile usage entirely.
Takeaway: Kudu is a fully featured server administration console. Restrict its SCM access URL by IP at the platform level β it's one checkbox in App Service Networking that most teams skip.