Scope and reading guide
This article consolidates a July-August 2026 ASE v3 investigation. The original goal was to react to front-end CPU and memory metrics. The investigation instead found missing metric samples and substantial scaling delays, and moved toward scheduled, proactive capacity. Read the final design direction before implementing the earlier experimental runbook.
Measurements, billing deductions, portal behavior, and API quirks below describe the source test environment. They are not a Microsoft service-level guarantee, current price quote, or assurance of identical behavior in another subscription. Confirm current product documentation, regional availability, permissions, and billing before production use.
The test configuration used an external-VIP ASE v3 in East US, Linux I1v2 workers, a Node.js 22 LTS sample application, and an Automation Account with a system-assigned managed identity. The front-end size reported by the service was Standard_D2d_v4. Actual environment identifiers have been replaced by variables.
# Supply these values in your own shell; do not commit them.
: "${SUBSCRIPTION_ID:?Set SUBSCRIPTION_ID}"
: "${RESOURCE_GROUP:?Set RESOURCE_GROUP}"
: "${ASE_NAME:?Set ASE_NAME}"
: "${LOCATION:?Set LOCATION}"
az account set --subscription "$SUBSCRIPTION_ID"
ASE_ID="/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Web/hostingEnvironments/$ASE_NAME"
az quota show --resource-name I1v2 \
--scope "/subscriptions/$SUBSCRIPTION_ID/providers/Microsoft.Web/locations/$LOCATION" \
--query "properties.limit.value" -o tsv
Check quota before creating an ASE. The original attempt in East US 2 had zero Isolated v2 quota; a quota update returned QuotaNotAvailableForResource. An available region with quota was used instead. Regional SKU availability does not itself establish that a particular subscription has usable quota. An empty ASE can still incur a minimum charge.
Choose the right resource and signal
| Resource type | What it represents | Relevant signals |
|---|---|---|
Microsoft.Web/hostingEnvironments | ASE infrastructure and front ends | TotalFrontEnds; other definitions may exist without samples |
Microsoft.Web/serverfarms | App Service Plan workers | CPU, memory, HTTP queue |
Microsoft.Web/sites | Individual applications | Requests and response time |
The ASE metric definitions listed CpuPercentage, MemoryPercentage, DiskQueueLength, and TotalFrontEnds. Over 72 hourly periods, CPU and memory had zero periods with actual samples, while TotalFrontEnds had 72. Querying the Instance dimension did not produce CPU time series either.
A 12-minute load test at approximately 180 requests/second produced application requests and response-time samples, worker CPU of 12-14%, worker memory of 30-31%, and a worker HTTP queue of zero. ASE CPU and memory still had no samples. The source also observed missing Linux socket/TCP metric samples. Do not treat a metric definition as proof that the deployment emits that metric.
az monitor metrics list-definitions --resource "$ASE_ID" \
--query "[].{name:name.value,unit:unit}" -o table
az monitor metrics list --resource "$ASE_ID" \
--metrics CpuPercentage MemoryPercentage TotalFrontEnds \
--interval PT1M --aggregation Average Count -o json
: "${START_UTC:?Set START_UTC to an ISO 8601 UTC timestamp}"
: "${END_UTC:?Set END_UTC to an ISO 8601 UTC timestamp}"
az rest --method get \
--url "https://management.azure.com${ASE_ID}/providers/Microsoft.Insights/metrics?api-version=2023-10-01&metricnames=CpuPercentage,TotalFrontEnds×pan=${START_UTC}/${END_UTC}&interval=PT1M&aggregation=Average,Count"
Missing is not zero
The source saw missing intervals represented as average: 0, count: 0. Include Count in compatible metric queries and use only points with count > 0. Filtering only on a non-null average can turn missing measurements into apparent idle capacity and trigger an unsafe scale-in. Use the metric's supported aggregation: application request volume is a total, not worker CPU's average.
Use application Requests as a demand proxy; worker CPU, memory, and HttpQueueLength provide supporting signals. Worker CPU is not front-end CPU. AverageResponseTime can indicate degraded application experience, but cannot alone isolate a front-end bottleneck.
Portal inspection
In the observed portal, the ASE blade did not expose a Metrics entry. Open Monitor → Metrics → Select a scope, choose the appropriate resource group, filter to App Service Environments, and select the ASE. Plot Total Front Ends. A flat line after resetting the minimum helped confirm that the configured floor persisted. Missing CPU graphs must not be interpreted as zero CPU.
Configured, reported-ready, and actual capacity
az rest --method get \
--url "https://management.azure.com${ASE_ID}/configurations/ingress?api-version=2023-12-01" \
--query "properties.{min:minimumInstanceCount,ready:readyInstanceCount,state:provisioningState}" -o json
az rest --method get \
--url "https://management.azure.com${ASE_ID}/multiRolePools/default?api-version=2023-12-01" \
--query "properties.instanceNames" -o json
| Value | Interpretation in this investigation |
|---|---|
minimumInstanceCount | The configured floor, not an instantaneous instance count. |
readyInstanceCount | A useful leading signal, but it advanced before the observed instance list. |
multiRolePools/default → instanceNames | The instance list used to measure actual convergence. Do not publish its addresses. |
TotalFrontEnds | Useful for trends and alerts; aggregation, ingestion delay, and missing intervals affect precision. |
provisioningState | Not sufficient evidence that newly requested front ends are available. |
After a 2-to-4 request, provisioningState was already Succeeded at four minutes, while the reported ready count was still two at fourteen minutes. In another 2-to-3 experiment, the ready count became three at eight minutes and four at nine minutes, while the actual list still contained two instances. Actual capacity reached four around twenty-one minutes and settled at three around forty minutes.
Consequently, a guard based only on provisioningState == Succeeded is insufficient. A mismatch between ready and minimum is also not the final convergence check. Use the actual instance list alongside the configured floor and ready signal. If ready exceeds actual capacity, the platform may already be scaling: avoid repeatedly increasing the target.
The ingress response does not provide a last-change timestamp. Explicit cooldowns require external state, such as an Automation Variable. The source saw temporarily empty ingress and role-pool responses during scaling; fail visibly or defer with an operational warning rather than interpreting an empty response as zero instances.
The tested capacities/compute and capacities/virtualip endpoints returned HTTP 400; workerPools was not usable for this ASE v3 scenario. The legacy frontEndScaleFactor is not the ASE v3 control described here.
Changing the minimum safely
Observed scale-out time
Three recorded experiments / elapsed minutes / not an SLA
# This changes capacity and can incur charges. Review the value first.
az rest --method put \
--url "https://management.azure.com${ASE_ID}/configurations/ingress?api-version=2023-12-01" \
--headers "Content-Type=application/json" \
--body '{"properties":{"minimumInstanceCount":3}}'
The source observed a 2022-09-01 PUT appearing successful, then reverting roughly 35 seconds later. Including a fuller request body did not resolve it, and the activity log did not show a useful failure. PUT requests using 2023-12-01 or 2024-04-01 persisted in that environment. These examples therefore use 2023-12-01. Confirm the supported API version for your deployment and use repeated GETs to establish a stable result; early reads sometimes returned an older value.
| Operation | Observed elapsed time | Behavior |
|---|---|---|
| 2 → 4 | 38 minutes | Actual count jumped to the target rather than stopping at three. |
| 2 → 3 | 20 minutes | Temporary overshoot was observed. |
| 3 → 4 | 35 minutes | Similar latency despite a smaller increment. |
| 4 → 2, after load | 4 → 3 at 15 minutes; 3 → 2 at 4 hours 15 minutes | Stepwise scale-in. |
| 4 → 2, idle experiment | 4 → 3 at 3 hours 36 minutes; no 3 → 2 by 6 hours 34 minutes | No deterministic scale-in deadline. |
The source inferred a roughly 30-minute decision cadence plus roughly ten minutes of execution. A planning allowance of at least forty minutes was recommended for the observed environment, not guaranteed as a universal upper bound. Explicit PUTs did not bypass the platform loop. Repeated small increases can compound delay; choose a sufficient target up front.
Raising a floor requests capacity; lowering it merely permits the platform to release capacity. The two operations are not symmetric. A floor of two remained unchanged for sixty hours across 245 samples. Lower the configured floor after an event rather than leaving it at the highest historical setting. Slow, stepwise scale-in is not proof of zero connection impact: consider long-lived connections and background workloads.
Billing observations and cost controls
The following formula and prices were inferred from the source's July-August 2026 billing and retail-price comparison. They are not a contractual pricing specification. Confirm current meters, region, currency, discounts, and actual charges in your subscription.
| Resource-ID suffix | Source interpretation |
|---|---|
isolatedv2mininstancefee_for_{ASE_NAME} | Minimum charge for an empty ASE. |
isolatedv2frontendmininstancefee_for_{ASE_NAME} | Additional configured front-end minimum above two. |
The observed East US Linux I1v2 worker price was USD 0.386/hour. The front-end fee matched the Windows I1v2 retail rate of USD 0.547/hour. Source billing suggested:
Observed additional front-end cost
= max(minimumInstanceCount - 2, 0) × configured hours × regional rate
A full day with minimum three produced 24 billable units and USD 13.128. A day with brief minimum-three/four settings produced two billable units and USD 1.094. These two rows totalled 26 units and USD 14.222. The source inferred billing followed the configured minimum rather than the instantaneous observed count: actual capacity remained elevated for hours after the floor was lowered, without matching extra front-end units. Temporary platform overshoot likewise did not produce additional units in those observations.
| Configured minimum | East US, 30 days (USD, approximate) | Korea Central, 30 days (KRW, approximate) |
|---|---|---|
| 2 | 0 additional | 0 additional |
| 3 | 394 | 633,118 |
| 5 | 1,182 | 1,899,353 |
| 10 | 3,151 | 5,064,941 |
| 20 | 7,089 | 11,396,117 |
The source's Korea Central retail rates were USD 0.573 or KRW 879.33 per front-end unit-hour, and USD 0.412 or KRW 632.26 per Linux worker-hour. Currency-specific retail prices are not simply a user-applied exchange rate. Totals above use the quoted rates and 720 hours, rounding only at the end; they exclude workers and other services.
In Cost Management + Billing → Cost analysis, group by Resource and inspect the full resource-ID tooltip. Front-end and worker charges can share the I1 v2 App meter, so grouping only by Meter or Meter Category can obscure the distinction. The front-end billing resource can display the ASE's name and resource type even though it is a separate charge entry.
# Public retail-price endpoint; no subscription identifiers required.
curl --fail --silent --show-error \
"https://prices.azure.com/api/retail/prices?\$filter=serviceName%20eq%20'Azure%20App%20Service'%20and%20armRegionName%20eq%20'eastus'%20and%20contains(meterName,'I1%20v2')"
Follow pagination if the retail-price response has a next-page link and select the applicable SKU and price type. Cost Management API calls are rate-limited; handle 429 responses and retry after the indicated delay.
Earlier metric-driven Automation design
The source retained a published PowerShell runbook but disabled its reactive schedule after the timing investigation. The runbook source itself is not in the repository; this section documents its reported behavior, not a downloadable or production-ready implementation.
Authentication used Connect-AzAccount -Identity. Monitoring Reader was assigned at ASE scope. Ingress writes required App Service Environment Contributor; Website Contributor alone lacked Microsoft.Web/hostingEnvironments/configurations/write. The experiment also had broader resource-group roles for app/plan manipulation and earlier monitoring experiments; do not copy unused broad assignments into production.
: "${AUTOMATION_PRINCIPAL_ID:?Set the managed identity principal ID}"
az role assignment create \
--assignee-object-id "$AUTOMATION_PRINCIPAL_ID" \
--assignee-principal-type ServicePrincipal \
--role "App Service Environment Contributor" --scope "$ASE_ID"
- Authenticate with the managed identity and select the subscription.
- Read actual front-end count from
multiRolePools/defaultand ingress configuration. - Defer when provisioning is incomplete, actual count is below the floor, or ready count exceeds actual count. Do not rely only on provisioning state.
- Enumerate the ASE's applications and plans; aggregate application requests and examine maximum worker CPU, memory, and queue signals. Reject missing samples.
- Compute requests per front end per minute. If no valid signals exist, retain current capacity and emit an operational diagnostic.
- Apply threshold logic, bounds, and optional dry-run output. Scale-in requires known low signals; missing data must not enable scale-in.
- Write ingress only when a reviewed desired floor differs from the current one.
| Parameter | Source default |
|---|---|
| Scale-out / scale-in requests per front end per minute | 6,000 / 1,500 |
| Worker CPU / memory scale-out threshold | 70% / 75% |
| Worker HTTP queue scale-out threshold | 10 |
| Floor / ceiling | 2 / 10 |
| Scale-out / scale-in increment | 2 / 1 |
| Lookback | 5 minutes |
| Enable scale-in | False; an explicit opt-in |
| WhatIfOnly | Dry-run mode without a capacity write |
The reported dry runs exercised no change, a near-threshold no-change case, 3-to-5 scale-out, suppressed scale-in, and opted-in 3-to-2 scale-in. These are historical source tests, not validation performed for this site. In PowerShell helpers, diagnostics written with Write-Output can contaminate a scalar return value; use a separate diagnostic stream such as Write-Verbose and maintain a clear return contract.
The historical notes describe draft upload, publication, job invocation, output/stream inspection, and a 15-minute schedule experiment through the Automation REST API 2023-11-01. Later notes explicitly disable the reactive schedule. Treat that later state as authoritative within the source. Verify currently supported scheduling intervals and use a supported trigger for sub-hourly execution; do not assume that a historical frequency: Minute request is a supported production scheduling contract.
: "${AUTOMATION_ACCOUNT:?Set AUTOMATION_ACCOUNT}"
: "${RUNBOOK_NAME:?Set RUNBOOK_NAME}"
: "${JOB_ID:?Set an existing job ID to inspect}"
AA_URL="https://management.azure.com/subscriptions/$SUBSCRIPTION_ID/resourceGroups/$RESOURCE_GROUP/providers/Microsoft.Automation/automationAccounts/$AUTOMATION_ACCOUNT"
az rest --method get --url "$AA_URL/runbooks/$RUNBOOK_NAME?api-version=2023-11-01" \
--query "properties.{state:state,params:parameters}" -o json
az rest --method get --url "$AA_URL/jobs/$JOB_ID?api-version=2023-11-01" \
--query "properties.{status:status,exception:exception}" -o json
az rest --method get --url "$AA_URL/jobs/$JOB_ID/output?api-version=2023-11-01"
az rest --method get --url "$AA_URL/jobs/$JOB_ID/streams?api-version=2023-11-01"
Recommended direction from the investigation
Reactive scaling cannot reliably absorb a traffic spike that rises within minutes when the requested front ends arrive tens of minutes later. The proposed replacement was a desired-state schedule: a stable base, optional daily windows, and explicit event overrides, reconciled regularly.
{
"timeZone": "Korea Standard Time",
"default": 2,
"daily": [
{"days": "Mon-Fri", "from": "08:00", "to": "20:00", "min": 3}
],
"overrides": [
{
"start": "2030-01-15T08:00",
"end": "2030-01-15T13:00",
"min": 4,
"reason": "Example event",
"owner": "EVENT_OWNER"
}
]
}
This is an illustrative schema, not an active schedule or runnable implementation. Replace the example event dates and owner before use.
- Resolve overlapping requirements with the maximum requested floor, not last-write-wins.
- Apply a lead-time setting automatically. A forty-minute allowance means an 08:00 event needs a request by 07:20 in the observed planning model; measure a suitable margin for your own environment.
- Ignore expired overrides, warn about stale entries, and lower the minimum after events.
- Enforce an explicit maximum allowed setting to prevent expensive input errors.
- Convert schedule time zones explicitly; Automation jobs run in UTC.
- On invalid configuration, retain current capacity and alert an operator. Never silently reset to the base during an event.
- Use a recurring reconciler so one failed invocation can be retried, while avoiding conflicting changes during scale-out.
| Configuration store | Source assessment |
|---|---|
| One versioned JSON blob | Readable whole configuration, history and rollback; favored for a reviewed operational design. |
| Automation Variable | No additional resource; useful for experiments, but a single-line editor can increase input errors. |
| Table Storage | Convenient per-event rows; less convenient for reviewing the complete configuration. |
| App Configuration | Purpose-built configuration management, with a separate service cost. |
| Git plus pipeline | Review and approval history; consider emergency update latency. |
Open work in the source included implementing this replacement runbook, verifying lead-time behavior, selecting the store, repeating timing measurements under load, and determining whether a second PUT during scale-out resets or changes the in-progress operation. None of those unfinished items is presented here as completed.
Deployment and troubleshooting lessons
- Quota: inspect the target region before deployment; a supported region can still have zero usable quota.
- SCM reachability: distinguish a network timeout from an authentication response. Review the ASE networking requirements and restrict access to required sources/ports; do not copy broad Internet allow rules from a lab.
- Runtime: inspect
az webapp list-runtimes --os-type linux. Node 20 was rejected in the source environment; Node 22 LTS was selected from supported options. - Stopped app: deployment can succeed while the application remains stopped after earlier startup failures. Check application state and startup logs.
- Kudu 401: check the intended deployment authentication method. Do not automatically enable basic publishing credentials to bypass a failed authenticated deployment.
- Ingress 403: check the managed identity's ASE-scoped write permissions rather than assuming Website Contributor is sufficient.
- Absent metric samples: collect supported app/plan signals and log missing data; do not scale in on zero-filled gaps.
- Load shape: front ends proxy requests. A CPU-burning endpoint primarily stresses workers; it does not isolate front-end pressure. Only load-test an environment you are authorized to test.
The historical load run reported 126,458 HTTP 200 responses, a median latency of 0.81 seconds, and roughly 180 requests/second. The referenced sample application's status, CPU-burn, and memory-allocation endpoints and its deployment package are absent from the repository. Original screenshots, support records, and environment addresses are intentionally not distributed.
Resources
No standalone companion files are distributed for this topic. Use the in-page examples and review the editorial notes below for source availability.
Editorial notes
This page consolidates the following source documents into an English technical guide:
- ASE v3 metric-driven minimum front-end instance adjustment and subsequent scheduled-capacity design
- Two original portal screenshots were omitted because they can expose subscription and environment details; their technical findings are described in text.
- Original subscription, tenant, principal, support-case, resource, host, IP, owner, and local-path identifiers are not published.
- The source refers to local runbooks, polling scripts, a cost-query script, and a sample application that are not present in the repository. They are not offered as downloads.
- Historical permission-bypass advice and broad network/basic-authentication changes are not reproduced as deployment recommendations.
No original credentials or private repository links are included. Do not put populated configuration files or copied production outputs back into this public site.