Deploy and Scale a Node.js App on Azure

Deploy a Node.js app to Azure App Service and configure CPU-based autoscaling.

Introduction

30 Second Summary

When your website goes viral or gets hit with unexpected traffic, you need it to handle the load without crashing. Cloud platforms solve this by automatically adding more servers when demand spikes and removing them when things calm down.

In this project, you will deploy a Node.js web application to Azure App Service and configure it to automatically scale up and down based on CPU usage.

What You'll Build

A Node.js Express web application deployed to Azure App Service with CPU-based autoscale rules that adjust instance count between 1 and 4.

By the end of this project, you'll have:

  • A live Node.js Express app deployed to Azure App Service and accessible via a public URL.
  • An App Service plan upgraded from Free to Standard tier with autoscale rules that scale out when CPU exceeds 70% and scale in when it drops below 30%.
  • Real-time scaling events observed in Azure Monitor after generating artificial CPU load.
  • Secret Mission: Add a deployment slot for zero-downtime staging deployments.

Are there any prerequisites?

You need an Azure account with free credits available. The project uses the Azure CLI and Node.js, which you will verify and install in Step 1.

Set Up Your Environment

Before you can deploy an app to the cloud, you need the right tools installed locally. This project uses the Azure CLI to manage cloud resources and Node.js to build the application.

In this step, you will verify both tools are ready, sign into your Azure account, and scaffold a new Express project that you will deploy in the next step.

In this step, get ready to:
  • Verify and install the Azure CLI, then sign in to your Azure account.
  • Verify Node.js is installed at version 18 or higher.
  • Create the project folder and scaffold the Express app.
Verify and install Azure CLI

The Azure CLI is a command-line tool that lets you create and manage Azure resources directly from your terminal.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check if the Azure CLI is already installed by running:
az version

Check the output against the tabs below.

✔️ I see a version number

You should see JSON output that includes a line like "azure-cli": "2.x.x".

Your Azure CLI is ready. Move on to signing in below.

ⓧ Command not found

The Azure CLI is not installed yet. Install it using the instructions for your operating system.

Windows

  • Install the Azure CLI by running:
winget install --exact --id Microsoft.AzureCLI
  • Close and reopen PowerShell after the installation finishes.
  • Verify the installation by running az version again.
  • You should see JSON output with an azure-cli version number.

macOS

  • Install the Azure CLI using Homebrew by running:
brew update && brew install azure-cli
  • Verify the installation by running az version again.
  • You should see JSON output with an azure-cli version number.

Now sign into your Azure account. This command opens your browser for authentication.

  • Sign in to Azure by running:
az login

What happens when you run this?

Your browser opens an Azure sign-in page. Sign in with the account that has your free credits.

After authentication, the terminal displays a list of your subscriptions in JSON format.

  • Confirm your subscription is active by running:
az account show

You should see JSON output that includes your subscription name and a "state": "Enabled" field.

Not seeing your subscription?

Make sure you signed in with the Azure account that has free credits attached. If you have multiple accounts, run az login again and pick the correct account in the browser.

help me troubleshoot my Azure CLI login

Verify Node.js is installed

Node.js is the JavaScript runtime that powers your Express server. This project requires version 18 or higher.

  • Check your Node.js version by running:
node --version

Check the output against the tabs below.

✔️ I see version 18 or higher

You should see something like v18.x.x, v20.x.x, or v22.x.x. Any of these work.

Node.js is ready. Move on to verifying npm below.

ⓧ I see a version lower than 18

You have an older version of Node.js installed. You need to upgrade to version 18 or higher.

Windows

  • Upgrade Node.js by running:
winget install --exact --id OpenJS.NodeJS.LTS
  • Close and reopen your terminal after the installation finishes.
  • Verify by running node --version again. You should now see v18 or higher.

macOS

  • Upgrade Node.js using Homebrew by running:
brew install node@lts
  • Close and reopen your terminal after the installation finishes.
  • Verify by running node --version again. You should now see v18 or higher.

ⓧ Command not found

Node.js is not installed on your machine. Install it using the instructions for your operating system.

Windows

  • Install Node.js by running:
winget install --exact --id OpenJS.NodeJS.LTS
  • Close and reopen your terminal after the installation finishes.
  • Verify by running node --version. You should see v18 or higher.

macOS

  • Install Node.js using Homebrew by running:
brew install node@lts
  • Close and reopen your terminal after the installation finishes.
  • Verify by running node --version. You should see v18 or higher.
  • Verify that npm (the Node.js package manager) is available by running:
npm --version

You should see a version number like 9.x.x or 10.x.x. npm is installed automatically alongside Node.js.

npm command not found?

If you installed Node.js but npm is missing, try closing and reopening your terminal. The path updates require a fresh session.

help me fix my npm installation

Create the project folder and scaffold the Express app

Now that your tools are ready, you will create a project folder and initialize a Node.js application with Express as a dependency.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir scalable-app
cd scalable-app
  • Confirm the folder was created by running:
ls

You should see the scalable-app folder listed. Since you already moved into it, the output will be empty (an empty folder).

  • Initialize a new Node.js project by running:
npm init -y

What does this command do?

The npm init -y command creates a package.json file in your folder. This file tracks your project's name, version, and dependencies.

The -y flag accepts all the defaults so you do not have to answer prompts.

  • Install Express as a dependency by running:
npm install express

You should see output confirming that packages were added. Your package.json now includes Express in its dependencies.

  • Confirm Express was added by running:
cat package.json

You should see "express" listed under the "dependencies" section with a version like ^4.21.2.

Not seeing express in your dependencies?

Make sure you are inside the scalable-app folder when you run npm install express. Run pwd to check your current location.

help me fix my npm install issue

✔️ Awesome, I've got everything!

Great. Double check you have the scalable-app folder on your Desktop with package.json and a node_modules folder inside it.

ⓧ I'd like to double check the full code

Your package.json should look like this (you may see additional fields like keywords, author, and license which are fine to keep):

{
  "name": "scalable-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "dependencies": {
    "express": "^4.21.2"
  }
}

Your environment is ready. Next up, you will build the Express application and deploy it live to Azure App Service.

Build and Deploy to Azure App Service

In the previous step, you set up your development environment and scaffolded a new Node.js project.

Now it is time to write the actual application code and push it live to the internet. You will build an Express server with endpoints for health checks and CPU load testing, then deploy it to Azure App Service on the Free tier.

In this step, get ready to:
  • Create a Node.js Express application with health and load-testing endpoints.
  • Deploy the app to Azure App Service on the Free tier.
  • Understand the Free tier's limitations for scaling.
Create the Express application

Your app needs three endpoints: one that returns status information, one for health checks, and one that generates CPU load on demand. The load endpoint will be essential later when you test autoscaling.

  • Create a new file called app.js in your scalable-app folder.
  • Add the following code to set up Express and define the root endpoint:
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.json({
    status: "healthy",
    message: "Scalable App on Azure App Service",
    timestamp: new Date().toISOString(),
    instance: process.env.WEBSITE_INSTANCE_ID || "local"
  });
});

What does this code do?

This imports Express and creates a server that listens on the port Azure provides (via the PORT environment variable) or falls back to port 3000 locally.

The root endpoint returns JSON with the app's health status, a timestamp, and the WEBSITE_INSTANCE_ID environment variable. Azure App Service sets this automatically so you can see which instance handled a request when multiple instances are running.

  • Below the root endpoint, add the health check and CPU load endpoints:
app.get("/health", (req, res) => {
  res.status(200).json({ status: "ok" });
});

app.get("/load", (req, res) => {
  const start = Date.now();
  // Simulate CPU-intensive work for ~500ms
  while (Date.now() - start < 500) {
    Math.sqrt(Math.random() * 999999);
  }
  res.json({
    message: "CPU load generated",
    duration: Date.now() - start + "ms",
    instance: process.env.WEBSITE_INSTANCE_ID || "local"
  });
});

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

What does this code do?

The /health endpoint returns a simple 200 OK response. Azure uses endpoints like this to confirm your app is running.

The /load endpoint runs a CPU-intensive loop for approximately 500 milliseconds. This simulates real workload so you can trigger autoscaling later in the project.

Finally, app.listen starts the server on the configured port.

  • Save app.js.

Now update package.json so Azure knows how to start your app.

  • Open package.json and replace its entire contents with the following:
{
  "name": "scalable-app",
  "version": "1.0.0",
  "description": "Scalable Node.js app on Azure App Service",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.21.2"
  }
}

Why update package.json?

Azure App Service uses the start script to launch your app. Without it, Azure would not know which file to run.

You also set main to app.js and added a description so the project metadata accurately reflects what you built.

  • Save package.json.
  • Test the app locally by running:
node app.js
  • Open your browser and visit http://localhost:3000.

You should see a JSON response with status, message, timestamp, and instance fields. The instance value shows local because you are not running on Azure yet.

  • Press Ctrl+C in your terminal to stop the server.

Don't see the JSON response?

  • Make sure you saved both app.js and package.json before running the command.
  • Check that port 3000 is not already in use by another application.
  • Look at your terminal for error messages. A missing express module means npm install did not complete in the previous step.

Why isn't my Express server starting on localhost?

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on.

ⓧ I'd like to double check the full code

Here is the complete app.js:

const express = require("express");
const app = express();
const port = process.env.PORT || 3000;

app.get("/", (req, res) => {
  res.json({
    status: "healthy",
    message: "Scalable App on Azure App Service",
    timestamp: new Date().toISOString(),
    instance: process.env.WEBSITE_INSTANCE_ID || "local"
  });
});

app.get("/health", (req, res) => {
  res.status(200).json({ status: "ok" });
});

app.get("/load", (req, res) => {
  const start = Date.now();
  // Simulate CPU-intensive work for ~500ms
  while (Date.now() - start < 500) {
    Math.sqrt(Math.random() * 999999);
  }
  res.json({
    message: "CPU load generated",
    duration: Date.now() - start + "ms",
    instance: process.env.WEBSITE_INSTANCE_ID || "local"
  });
});

app.listen(port, () => {
  console.log(`App listening on port ${port}`);
});

And the complete package.json:

{
  "name": "scalable-app",
  "version": "1.0.0",
  "description": "Scalable Node.js app on Azure App Service",
  "main": "app.js",
  "scripts": {
    "start": "node app.js"
  },
  "dependencies": {
    "express": "^4.21.2"
  }
}
Deploy to Azure App Service on the Free tier

Your app works locally. Now you will provision Azure resources and deploy it to the cloud. Azure App Service needs three things: a resource group to organize everything, an App Service plan to define the hosting infrastructure, and the web app itself.

  • Create a resource group by running this command:
az group create --name scalable-app-rg --location eastus

What is a resource group?

A resource group is a container in Azure that holds related resources. Everything you create for this project lives inside scalable-app-rg. When you want to clean up later, you can delete the group and everything inside disappears.

  • Create an App Service plan on the Free tier by running:
az appservice plan create --name scalable-app-plan --resource-group scalable-app-rg --sku F1 --is-linux

What is an App Service plan?

An App Service plan defines the compute resources your app runs on. The F1 SKU is the Free tier, which gives you shared compute at no cost. The --is-linux flag creates a Linux-based plan where Node.js runs natively.

Next, create the web app itself. Your app name must be globally unique across all of Azure because it becomes part of the public URL.

  • Choose a unique name for your app (only lowercase letters, numbers, and hyphens are allowed).
  • Create the web app by running:
az webapp create --resource-group scalable-app-rg --plan scalable-app-plan --name [[APP_NAME="your-unique-app-name"]] --runtime "NODE|20-lts"

What does this command do?

This creates a web app inside your resource group and plan, configured to run Node.js 20 LTS. The name you choose becomes the subdomain in your app's URL: https://your-app-name.azurewebsites.net.

Getting a name conflict error?

If Azure says the name is already taken, try adding random numbers or your initials to the name. For example: scalable-app-jd-2026.

Help me resolve an Azure App Service name conflict.

Now package and deploy your application files. Since this project uses az webapp deploy, you need to include all files the app needs to run in the zip (including the node_modules folder).

  • Create the zip archive by running:
Compress-Archive -Path app.js, package.json, node_modules -DestinationPath app.zip
  • Deploy the zip to your web app by running:
az webapp deploy --resource-group scalable-app-rg --name [[APP_NAME="your-unique-app-name"]] --src-path app.zip --type zip

What happens during deployment?

Azure uploads your zip file and extracts its contents to the app's filesystem at /home/site/wwwroot. Because you included node_modules in the zip, the app is ready to run immediately without needing a server-side npm install.

  • Wait for the deployment to complete. The command will output a JSON response when finished.
  • Verify the app is live by opening your browser and visiting:
https://[[APP_NAME="your-unique-app-name"]].azurewebsites.net

You should see the same JSON response as your local test, but now the instance field shows a real Azure instance ID instead of local.

Seeing an error page instead of JSON?

  • Azure Free tier apps can take 30-60 seconds to cold-start. Wait a moment and refresh.
  • Make sure your package.json has a start script set to node app.js. Azure relies on this to start the app.
  • Confirm the zip included node_modules. If Express is missing, the app crashes on startup.

Why is my Azure App Service returning an error page?

Confirm the Free tier limitations

Your app is live on the internet. However, the Free tier has strict constraints that prevent it from handling production traffic.

What are the Free tier limitations?

  • Your app runs on shared compute. Multiple Azure customers share the same underlying machine.
  • You get 60 CPU minutes per day. If your app uses more, it stops responding until the next day.
  • You cannot scale out. The Free tier is locked to a single instance, so there is no way to add more servers when traffic spikes.
  • Autoscale, custom domains, and deployment slots are all unavailable on this tier.

In the next step, you will upgrade to the Standard (S1) tier, which removes all of these limitations and unlocks autoscaling.

Your app is deployed and running on Azure. Next up, you will upgrade the plan to unlock autoscaling and dedicated compute.

Scale Up to Standard Tier

Your app is live on Azure, running on the Free tier. The Free tier is great for testing, but it limits you to shared compute and 60 CPU minutes per day.

To enable autoscaling, you need dedicated compute resources. In this step, you'll upgrade your App Service plan to the Standard (S1) tier, which unlocks horizontal scaling and several production features.

In this step, get ready to:
  • Upgrade your App Service plan from Free (F1) to Standard (S1).
  • Verify the tier change took effect.
  • Understand what the upgrade enables for your app.
Upgrade the App Service plan from F1 to S1

The Free tier cannot scale out to multiple instances. You need to move to at least the Standard tier to configure autoscaling.

  • Upgrade your App Service plan to Standard tier by running this command:
az appservice plan update --name scalable-app-plan --resource-group scalable-app-rg --sku S1

What does this command do?

  • az appservice plan update modifies an existing App Service plan in place.
  • --sku S1 changes the pricing tier to Standard S1, which provides dedicated compute with 1 core and 1.75 GB RAM.
  • Your web app stays deployed and running throughout the upgrade. Azure migrates it to the new tier automatically.

The command takes a moment to process. You should see JSON output showing the updated plan details once it completes.

Why Standard instead of Basic?

Azure's Basic (B1) tier provides dedicated compute but does not support autoscale. Microsoft's documentation states that you cannot set up autoscale for Free or Basic tiers.

Standard (S1) is the cheapest tier that supports autoscale, allowing up to 10 scale-out instances. It also unlocks deployment slots, custom domains, and SSL certificates.

Seeing a permission or subscription error?

  • Make sure you are signed in with az login and your session has not expired.
  • Confirm your subscription has available credit by running az account show and checking the subscription state is Enabled.
  • If you see a quota error, your subscription may restrict certain SKUs in the selected region. Try a different region or contact Azure support.

Why is my App Service plan upgrade failing?

Verify the tier change
  • Confirm the upgrade succeeded by running this command:
az appservice plan show --name scalable-app-plan --resource-group scalable-app-rg --query "sku"

What does this command do?

az appservice plan show retrieves the current details of your plan. The --query "sku" flag filters the output to show only the SKU information, making it easy to confirm the tier at a glance.

You should see output showing tier as Standard and name as S1.

  • Visit your app URL in the browser to confirm it is still running after the tier change.

App not responding after upgrade?

  • Give it 30 seconds. The app may take a moment to restart on the new tier.
  • Refresh the page. If you still see an error, check the app status with az webapp show --name <your-app-name> --resource-group scalable-app-rg --query "state".

Why is my app not responding after upgrading the App Service plan?

Understand what changed

Your app just moved from shared infrastructure to its own dedicated virtual machine. This is a significant upgrade in what your app can do.

What did the upgrade unlock?

  • Dedicated compute. Your app moved from shared CPU quotas (60 minutes/day) to a dedicated VM instance with 1 core and 1.75 GB RAM.
  • Autoscaling. The S1 tier supports horizontal scaling (scale out) with up to 10 instances. You will configure this in the next step.
  • Production features. Custom domains, SSL certificates, and deployment slots are now available.
  • Cost. The S1 plan costs approximately $69.35/month, prorated per hour while active. You will delete this resource at the end of the project to avoid ongoing charges.

Your app now runs on dedicated infrastructure that supports scaling. Next up, you'll configure autoscale rules that automatically add and remove instances based on CPU load.

Configure Autoscale Rules

Your app is running on a dedicated Standard tier instance. Right now it runs on exactly one instance no matter how much traffic hits it.

In this step, you will configure Azure Monitor autoscale rules that automatically add instances when CPU gets high and remove them when load drops. This is horizontal scaling in action.

In this step, get ready to:
  • Create autoscale settings on your App Service plan with instance limits.
  • Add a scale-out rule that adds an instance when CPU exceeds 70%.
  • Add a scale-in rule that removes an instance when CPU drops below 30%.
Create autoscale settings for the App Service plan

Before you can add scaling rules, you need to create an autoscale settings resource that attaches to your App Service plan. This resource defines the instance boundaries: how few, how many, and how many by default.

  • Retrieve your App Service plan's resource ID by running this command:
az appservice plan show --name scalable-app-plan --resource-group scalable-app-rg --query "id" --output tsv

What does this command do?

The --query "id" flag uses a JMESPath query to extract just the resource ID from the full JSON output. The --output tsv flag strips the quotes so you get a clean string you can copy.

  • Copy the resource ID from the output. It looks something like /subscriptions/.../resourceGroups/scalable-app-rg/providers/Microsoft.Web/serverfarms/scalable-app-plan.
  • Create the autoscale settings by running this command, replacing the resource ID placeholder with the value you just copied:
az monitor autoscale create --resource-group scalable-app-rg --resource [[PLAN_RESOURCE_ID="your App Service plan resource ID from the previous command"]] --min-count 1 --max-count 4 --count 1 --name scalable-app-autoscale

What does this command do?

This creates an autoscale settings resource named scalable-app-autoscale attached to your App Service plan.

  • --min-count 1 means Azure will never scale below 1 instance.
  • --max-count 4 caps scaling at 4 instances maximum.
  • --count 1 sets the default instance count when no rules are active.
  • You should see JSON output confirming the autoscale settings were created, including the name scalable-app-autoscale and your capacity limits.

Seeing a 'resource not found' error?

  • Make sure you pasted the full resource ID from the previous command. It should start with /subscriptions/ and end with scalable-app-plan.
  • Confirm the resource group name scalable-app-rg is spelled exactly as shown.

Help me debug my az monitor autoscale create command

Add a scale-out rule

The autoscale settings define instance boundaries, but without rules, Azure has no trigger to actually scale. You need a rule that tells Azure when to add instances.

  • Add a scale-out rule that triggers when average CPU exceeds 70% over 5 minutes by running:
az monitor autoscale rule create --resource-group scalable-app-rg --autoscale-name scalable-app-autoscale --scale out 1 --condition "CpuPercentage > 70 avg 5m"

What does this rule do?

This rule monitors the CpuPercentage metric across all instances in your App Service plan. When the average CPU usage stays above 70% for 5 minutes, Azure adds 1 instance.

The 5-minute window prevents scaling on brief CPU spikes. Only sustained high load triggers a new instance.

  • You should see JSON output confirming the rule was added to the default profile.

Command returned an error?

  • Check that --autoscale-name matches exactly: scalable-app-autoscale. A typo here means Azure cannot find the settings to attach the rule to.
  • Make sure the condition string is in double quotes and uses the exact format: "CpuPercentage > 70 avg 5m".

Help me fix my autoscale rule create command

Add a scale-in rule

Without a scale-in rule, instances added during high load would stay running forever. That means unnecessary cost. You need a matching rule to remove instances when CPU drops.

  • Add a scale-in rule that triggers when average CPU drops below 30% over 5 minutes by running:
az monitor autoscale rule create --resource-group scalable-app-rg --autoscale-name scalable-app-autoscale --scale in 1 --condition "CpuPercentage < 30 avg 5m"

Why pair scale-out with scale-in?

Azure best practices recommend using the same metric for both directions. If you scale out on CPU but scale in on a different metric, the two rules can conflict and cause flapping where instances are constantly added and removed.

With multiple rules, scale-out fires if ANY rule is met. Scale-in requires ALL rules to be met. Using one metric for both keeps the logic predictable.

  • Verify your complete autoscale configuration by running:
az monitor autoscale show --resource-group scalable-app-rg --name scalable-app-autoscale
  • Confirm the output shows your capacity limits (min 1, max 4, default 1) and two rules in the default profile: one scale-out rule for CPU > 70% and one scale-in rule for CPU < 30%.

Only seeing one rule in the output?

  • Scroll through the JSON output. The rules array should contain two entries. If you only see one, re-run the missing rule command and check for typos in --autoscale-name.
  • If you accidentally created the rule with the wrong threshold, delete it with az monitor autoscale rule delete and recreate it.

Help me verify my autoscale rules are configured correctly

Your autoscale rules are live. Next up, you will generate CPU load to trigger scaling and watch Azure add instances in real time.

Test Scaling and Monitor

Your autoscale rules are configured and waiting for action. The true test of any scaling configuration is watching it respond to real load.

In this step, you will generate sustained CPU pressure on your app by hammering the /load endpoint. Then you will watch Azure Monitor detect the spike and spin up additional instances automatically.

In this step, get ready to:
  • Generate sustained CPU load against your live app using PowerShell loops.
  • Observe a scale-out event in Azure Monitor Run history.
  • Stop the load and confirm scale-in returns the instance count to 1.
Generate CPU load to trigger scale-out

Your /load endpoint runs a CPU-intensive computation for about 500 milliseconds per request. To push average CPU above the 70% threshold, you need to send many requests in rapid succession across multiple terminal windows.

  • Open PowerShell (you should already have it open from the previous step).
  • Generate load against your app by running this command:
for ($i=1; $i -le 100; $i++) { Invoke-WebRequest -Uri "https://[[APPNAME="your-app-name"]].azurewebsites.net/load" -UseBasicParsing | Out-Null; Start-Sleep -Milliseconds 100 }

What does this command do?

  • The for loop sends 100 requests to your /load endpoint, pausing 100 milliseconds between each one.
  • Each request forces the server to burn CPU for ~500ms, creating sustained pressure on the App Service instance.
  • -UseBasicParsing skips rendering the response in a browser engine, keeping the loop lightweight on your local machine.
  • Out-Null discards the response output so your terminal stays clean.
  • Open 2-3 additional PowerShell windows and run the same command in each one simultaneously.

Why multiple windows?

A single loop sends requests sequentially. Since the server takes ~500ms to respond, one loop alone only keeps the CPU busy part of the time. Running 3-4 loops in parallel ensures requests overlap and sustain CPU above the 70% threshold for the full 5-minute evaluation window.

  • Let all loops run to completion. Each loop takes about 60 seconds to finish (100 requests x ~600ms each).
  • If the loops finish before 5 minutes have passed, re-run them. The autoscale rule needs CPU to stay above 70% for a sustained 5-minute window.

Seeing connection errors?

  • Make sure you replaced the app name in the URL with your actual app name (the same one you used in Step 2).
  • Check that your app is still running by visiting the root URL in your browser.
  • help me troubleshoot connection errors when hitting my Azure App Service /load endpoint
Observe scaling in the Azure Portal

While your loops are generating load (or shortly after), you can check the Azure Portal for evidence that autoscale kicked in. Scaling events may take 5-10 minutes to appear after the CPU threshold is first sustained.

  • Open the Azure Portal in your browser.
  • In the search bar at the top of the page, type Monitor and select Monitor from the results.
  • In the left sidebar, select Autoscale.
  • Click on scalable-app-plan in the resource list to open its autoscale settings.
  • Select the Run history tab at the top of the page.

What should I see in Run history?

The Run history tab shows a timeline of scaling events. When autoscale triggers, you will see an entry showing the observed capacity increasing from 1 to 2 (or more) instances. Each entry logs the timestamp, the scale action taken, and the reason (CPU exceeded the threshold).

If you do not see an event yet, wait a few more minutes. The autoscale engine evaluates metrics every 5 minutes, so the first scale-out can take up to 10 minutes from when sustained load began.

  • Wait until you see at least one scale-out event in the Run history showing the instance count increased above 1.

Check instance count from the CLI

You can also confirm the current instance count from PowerShell by running:

az monitor autoscale show --resource-group scalable-app-rg --name scalable-app-autoscale --query "targetResourceUri" --output tsv

Or visit your app's root URL multiple times and watch the instance field in the JSON response. When multiple instances are active, you may see different WEBSITE_INSTANCE_ID values across requests.

Stop the load and observe scale-in

Now that you have confirmed scale-out works, you need to verify the other half of your configuration. When load drops, Azure should remove instances to save costs.

  • Stop all running PowerShell loops by pressing Ctrl+C in each terminal window, or simply close the extra windows.
  • Wait 5-10 minutes for CPU to drop below the 30% threshold and stay there for the 5-minute evaluation window.
  • Return to the Run history tab in the Azure Portal.
  • Look for a scale-in event showing the instance count decreased back to 1.

Why does scale-in take longer?

Scale-in requires ALL scale-in rules to be met simultaneously. The 5-minute evaluation window means Azure needs to observe CPU below 30% for a full 5 minutes before removing an instance. This conservative approach prevents thrashing where instances are constantly added and removed.

  • Confirm the instance count has returned to 1 by checking the Run history for the scale-in entry.

Not seeing scale-in after 15 minutes?

  • Make sure all your PowerShell loops have stopped. Any background requests keep CPU elevated.
  • Refresh the Run history page. Events do not auto-refresh in the portal.
  • Check that your scale-in rule uses CpuPercentage < 30. If you set a lower threshold, it takes longer for idle CPU to drop that far.
  • help me understand why my Azure autoscale is not scaling in after I stopped generating load

You have now seen horizontal scaling in action. Your app automatically grew to handle demand and shrank when the pressure lifted. This is the core pattern behind every production autoscaling setup on Azure App Service.

Secret mission

Add a Deployment Slot for Zero-Downtime Deploys

In production, deploying directly to a live server means your users experience downtime during every update. Can you set up a staging deployment slot, deploy a new version there, and swap it into production with zero downtime?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. The S1 App Service plan costs approximately $69.35/month (prorated per hour) while it remains active, so cleanup is important once you are done experimenting.

Cost warning

Your App Service plan is on the S1 (Standard) tier, which costs approximately $69.35/month even when no traffic is hitting the app. If you leave it running, it will consume your Azure free credits. Delete the resource group when you are finished to stop all charges immediately.

Resources you used:

  • Resource group scalable-app-rg (contains App Service plan, web app, autoscale settings, and deployment slot).
  • Local project folder scalable-app on your Desktop.

Keep everything running

No action needed. Choose this if you are still actively building or experimenting with scaling rules.

  • Monitor your Azure credit usage in the Azure Portal under Cost Management + Billing.
  • Your S1 plan charges approximately $0.09/hour while active.
  • Azure CLI and Node.js remain installed on your machine for future projects.

Pause - I'll come back to this later

Delete the Azure resources to stop all charges, but keep your local project files so you can redeploy when you are ready to return.

  • Delete the resource group (and everything inside it) by running this command in PowerShell:
az group delete --name scalable-app-rg --yes --no-wait

This removes the App Service plan, web app, autoscale settings, and deployment slot in one command. Your local scalable-app folder stays on your Desktop so you can redeploy later.

Delete - I don't want to use this again

Remove all project resources from Azure and delete the local project folder to start completely fresh.

  • Delete the resource group and all Azure resources by running:
az group delete --name scalable-app-rg --yes --no-wait

This deletes the resource group and everything inside it (App Service plan, web app, autoscale settings, and deployment slot).

  • Delete your local project folder by running:
cd ~/Desktop
Remove-Item -Path scalable-app -Recurse -Force

Your machine is now clean of all project resources.

That's a Wrap!

That's a Wrap!

Nice work! You just deployed a live, autoscaling web application to Azure App Service and watched it respond to real-world load in real time.

You've learned how to:

  • Deployed a Node.js Express app to Azure App Service using the Azure CLI.
  • Configured CPU-based autoscale rules that automatically add and remove instances based on real-time metrics.
  • Generated load and observed horizontal scaling events in Azure Monitor.
  • Set up a deployment slot for zero-downtime staging deployments (Secret Mission).

Ready to quiz yourself?