Skip to main content

Scale your app up and out with autoscale

In this lab, you take a running web app on Azure App Service from a single small instance to an app that grows and shrinks with demand. You start on the Basic B1 tier, scale up to Standard S1 for more power per instance, then turn on autoscale so App Service adds and removes instances automatically based on CPU.

Scaling happens at the App Service plan level, so everything here is language-agnostic. Any app on the plan - .NET, Node.js, Python, Java, PHP, or a custom container - scales the same way. You'll deploy a minimal app just so there's something to scale.

You'll do it three ways so you can pick the workflow that fits you:

  • Azure Developer CLI (azd) - declare the plan tier and autoscale rules in Bicep, then provision.
  • Azure CLI (az) - az appservice plan update to scale up, then az monitor autoscale to add rules.
  • Azure portal - the visual Scale up and Scale out blades on the plan.
App Service Labs complements Microsoft Learn

This lab is a hands-on, end-to-end walkthrough. For reference depth on any concept, follow the "Learn more" links to the official Microsoft Learn docs.

Estimated time: 30-40 minutes

What you'll build

A single web app that starts on a B1 plan (about USD 13/month), scaled up to an S1 plan (about USD 70/month), with an autoscale setting that keeps 1 to 3 instances running: it scales out when average CPU goes above 70 percent and scales back in when CPU drops below 30 percent, plus a scheduled profile that guarantees extra instances during business hours.

Objectives

By the end of this lab you will be able to:

  • Explain the difference between scale up (vertical) and scale out (horizontal).
  • Scale an App Service plan up from B1 to S1.
  • Create an autoscale setting with a CPU metric rule to scale out and scale in.
  • Add a scheduled autoscale profile for predictable load.
  • Recognize Always On and health check as related performance settings.

Prerequisites

Before you begin, you will need an Azure subscription with Owner permissions and a GitHub account.

In addition, you will need the following tools installed on your local machine:

Setup Azure CLI

Start by logging into Azure by run the following command and follow the prompts:

az login --use-device-code
tip

You can log into a different tenant by passing in the --tenant flag to specify your tenant domain or tenant ID.

Region and tier

This lab uses the East US region. Autoscale requires the Standard tier or higher, so you scale up to S1 before turning it on. Pick the smallest tier that shows the scenario, and change the region to one near you if you prefer.

Scale up vs scale out

These two words sound similar but mean different things. Getting them straight is the whole point of this lab.

  • Scale up (vertical) changes the pricing tier of the App Service plan - for example B1 to S1 to P1v3. Each instance gets more CPU, memory, and features (custom domains, staging slots, autoscale, and more). You get a bigger box, not more boxes.
  • Scale out (horizontal) changes the number of instances running your app behind the built-in load balancer. More instances handle more concurrent traffic and add redundancy. Autoscale adjusts that instance count for you based on a schedule or a metric such as CPU.

You often do both: scale up to unlock a tier that meets your per-request needs, then scale out to handle concurrency. Autoscale (scale out and in) is only available on Standard and higher, which is why this lab moves from B1 to S1 first.

Deploy, scale up, and autoscale

Choose your path
Set these once - every matching step and code sample below follows your choice.
Tooling
Language

With the Azure Developer CLI you describe the plan tier and the autoscale rules in Bicep, so scaling is repeatable infrastructure as code. You provision on B1 first, then change the tier to S1 and add an autoscale resource, and re-provision.

1. Sign in

azd auth login

2. Create the project structure

mkdir asl-autoscale && cd asl-autoscale
mkdir infra src

Create azure.yaml in the project root:

# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: asl-autoscale
services:
web:
project: ./src
language: js # change per language: js, dotnet, python, java
host: appservice

Add a minimal app under src/. This lab uses Node.js; any stack works because scaling is at the plan level. See Deploy your first web app for the other languages.

Create src/server.js:

const http = require('http');
const port = process.env.PORT || 3000;
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/html' });
res.end('<h1>Hello from Azure App Service autoscale lab!</h1>');
}).listen(port);

Create src/package.json:

{
"name": "asl-autoscale",
"version": "1.0.0",
"main": "server.js",
"scripts": { "start": "node server.js" }
}

Create infra/main.parameters.json. The planSku parameter is what you change to scale up:

{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"environmentName": { "value": "${AZURE_ENV_NAME}" },
"location": { "value": "${AZURE_LOCATION}" },
"resourceGroupName": { "value": "${AZURE_RESOURCE_GROUP}" },
"planSku": { "value": "${PLAN_SKU=B1}" }
}
}

Create infra/main.bicep. It runs at subscription scope so azd creates and owns the resource group:

targetScope = 'subscription'

@description('Name of the azd environment; used to derive resource names.')
param environmentName string

@description('Azure region for all resources.')
param location string

@description('Resource group to create for this environment.')
param resourceGroupName string

@description('App Service plan SKU. Autoscale requires Standard (S1) or higher.')
param planSku string = 'B1'

@description('Deploy the autoscale setting. Requires a Standard or higher planSku.')
param enableAutoscale bool = false

resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
name: resourceGroupName
location: location
}

module resources 'resources.bicep' = {
name: 'resources'
scope: rg
params: {
location: location
environmentName: environmentName
planSku: planSku
enableAutoscale: enableAutoscale
}
}

output WEB_URI string = resources.outputs.webUri

Create infra/resources.bicep. The autoscalesettings resource holds the CPU rules and only deploys when enableAutoscale is true:

@description('Azure region for all resources.')
param location string

@description('azd environment name used to derive globally unique names.')
param environmentName string

@description('App Service plan SKU. Autoscale requires Standard (S1) or higher.')
param planSku string = 'B1'

@description('Deploy the autoscale setting.')
param enableAutoscale bool = false

var suffix = uniqueString(subscription().id, resourceGroup().id, environmentName)
var planName = 'plan-${suffix}'
var webName = 'app-${suffix}'

resource plan 'Microsoft.Web/serverfarms@2023-12-01' = {
name: planName
location: location
sku: {
name: planSku // change B1 to S1 to scale up
}
kind: 'linux'
properties: {
reserved: true // required for Linux plans
}
}

resource web 'Microsoft.Web/sites@2023-12-01' = {
name: webName
location: location
kind: 'app,linux'
tags: {
'azd-service-name': 'web' // links this site to the "web" service in azure.yaml
}
properties: {
serverFarmId: plan.id
httpsOnly: true
siteConfig: {
linuxFxVersion: 'NODE|22-lts' // change per language, for example DOTNETCORE|8.0
alwaysOn: true // keep the app loaded; needs Basic or higher
appSettings: [
{
name: 'SCM_DO_BUILD_DURING_DEPLOYMENT'
value: 'true'
}
]
}
}
}

resource autoscale 'Microsoft.Insights/autoscalesettings@2022-10-01' = if (enableAutoscale) {
name: 'autoscale-${webName}'
location: location
properties: {
enabled: true
targetResourceUri: plan.id
profiles: [
{
name: 'CPU based autoscale'
capacity: {
minimum: '1'
maximum: '3'
default: '1'
}
rules: [
{
metricTrigger: {
metricName: 'CpuPercentage'
metricResourceUri: plan.id
timeGrain: 'PT1M'
statistic: 'Average'
timeWindow: 'PT5M'
timeAggregation: 'Average'
operator: 'GreaterThan'
threshold: 70
}
scaleAction: {
direction: 'Increase'
type: 'ChangeCount'
value: '1'
cooldown: 'PT5M'
}
}
{
metricTrigger: {
metricName: 'CpuPercentage'
metricResourceUri: plan.id
timeGrain: 'PT1M'
statistic: 'Average'
timeWindow: 'PT5M'
timeAggregation: 'Average'
operator: 'LessThan'
threshold: 30
}
scaleAction: {
direction: 'Decrease'
type: 'ChangeCount'
value: '1'
cooldown: 'PT5M'
}
}
]
}
]
}
}

output webUri string = 'https://${web.properties.defaultHostName}'

3. Provision on B1 and deploy

Create an environment with a unique suffix, then provision and deploy in one step. planSku defaults to B1 and enableAutoscale is off:

SUFFIX=$(openssl rand -hex 3) # 6 lowercase hex chars
azd env new "asl-autoscale-${SUFFIX}" --location eastus
azd env set AZURE_RESOURCE_GROUP "rg-asl-autoscale-${SUFFIX}"
azd up

When it finishes, azd prints the app endpoint. Confirm it returns HTTP 200:

curl -I $(azd env get-value WEB_URI)

4. Scale up to S1 and turn on autoscale

Now raise the tier and enable the autoscale resource, then re-provision. Because the plan already exists, azd updates it in place:

azd env set PLAN_SKU S1
azd env set ENABLE_AUTOSCALE true
azd provision

azd reads ENABLE_AUTOSCALE into the enableAutoscale parameter through the environment. If your main.parameters.json doesn't map it yet, add this parameter line:

"enableAutoscale": { "value": "${ENABLE_AUTOSCALE=false}" }
Bool parameters from azd environment

azd environment values are strings. The Bicep @allowed-free bool parameter accepts true/false from ${ENABLE_AUTOSCALE=false}. If you hit a type error, set the value directly in main.parameters.json to true instead of using the environment variable.

Verify

Confirm all three outcomes.

The app still returns HTTP 200 after scaling up:

curl -I https://<your-app-name>.azurewebsites.net

Expected output (validated during authoring):

HTTP/1.1 200 OK
Content-Type: text/html

The plan is on the S1 (Standard) tier:

az appservice plan show \
--name <your-plan-name> \
--resource-group <your-rg> \
--query "sku.{name:name, tier:tier}" -o table

The autoscale setting exists, is enabled, and has both CPU rules:

az monitor autoscale show \
--name autoscale-<your-app-name> \
--resource-group <your-rg> \
--query "{enabled:enabled, min:profiles[0].capacity.minimum, max:profiles[0].capacity.maximum, rules:profiles[0].rules[].{metric:metricTrigger.metricName, op:metricTrigger.operator, threshold:metricTrigger.threshold, dir:scaleAction.direction}}" -o json

You should see enabled: true, a floor of 1 and ceiling of 3, and two CpuPercentage rules - one GreaterThan 70 that increases count, one LessThan 30 that decreases it.

Watching a real scale-out takes time

Autoscale evaluates metrics on a schedule and honors a cooldown, so a real CPU-driven scale-out can take 10 or more minutes under sustained load. For this lab, the created and enabled rules are the success signal. To watch scaling live, generate load (for example with a load test) and open Scale out (App Service plan) > Run history to see instance count change.

While you're tuning performance, two more plan-level settings are worth knowing. They aren't autoscale, but they pair with it:

  • Always On keeps your app loaded so it doesn't unload after idle time, which avoids slow cold starts on the first request. It's available on Basic and higher. Turn it on under Settings > Configuration > General settings.
  • Health check pings a path you choose (for example /healthz) and removes unhealthy instances from the load balancer rotation - especially useful once you scale out to multiple instances. Configure it under Settings > Health check.

Clean up resources

To avoid ongoing charges, delete the resources when you're done. Deleting the resource group removes the web app, its plan, and the autoscale setting.

azd down --force

Summary

In this lab, you took an app from one small instance to an elastic app on App Service. You learned how to:

  • Tell scale up (bigger instances, a higher tier) apart from scale out (more instances).
  • Scale a plan up from B1 to S1 to unlock Standard-tier features, including autoscale.
  • Create an autoscale setting with CPU rules to scale out above 70 percent and scale in below 30 percent, bounded to 1 to 3 instances.
  • Add a scheduled profile for predictable busy hours.
  • Recognize Always On and health check as complementary performance settings.

Troubleshooting

  • az monitor autoscale create fails with a usage error about --resource. Pass the plan's full resource ID to --resource and don't also pass --resource-type. Get the ID with az appservice plan show --name <plan> --resource-group <rg> --query id -o tsv.
  • Autoscale options are greyed out or the create command is rejected. Autoscale requires Standard or higher. Scale up to S1 first, then create the setting.
  • The app scales out but never scales back in. You likely have only a scale-out rule. Add a scale-in rule with a lower threshold (for example CPU less than 30) so instances return to the floor.
  • Instances "flap" up and down. Your scale-out and scale-in thresholds are too close, or the cooldown is too short. Widen the gap between thresholds and increase the cooldown.
  • azd type error on the autoscale bool parameter. azd environment values are strings. If ${ENABLE_AUTOSCALE=false} doesn't resolve to a Bicep bool, set "enableAutoscale": { "value": true } directly in infra/main.parameters.json.

For deeper guidance, see Get started with autoscale in Azure.

Learn more