The official documentation for Azure Monitor Autoscale lacked a critical detail regarding how cooldown periods are evaluated.
I discovered this gap during a real-world incident. My environment scaled down as expected when CPU usage dropped. However, 15 minutes later, an unexpected CPU spike hit the system. To my surprise, the environment did not scale out. The scale-out action was blocked, waiting for its own long cooldown period to expire, even though the last action was a scale-in.
I wanted to understand exactly how the engine was making these decisions, so after running specific tests and analyzing the AutoscaleEvaluationsLog, I discovered the underlying truth: cooldowns in Service Bus Autoscale are evaluated per rule, not per profile, and are counted from the last scale action, no matter the direction.
Because this wasn’t clearly stated, I submitted a Pull Request to the official Microsoft documentation to address the gap. You can view the PR here: https://github.com/MicrosoftDocs/azure-monitor-docs/pull/303.
The Microsoft team merged it, noting: “You caught a real gap in our docs.”
Here is a breakdown of what I found during those tests and how the Autoscale engine really behaves.
Looking Under the Hood
When configuring an Autoscale setting in the Azure Portal, you create a Profile and add rules (e.g., “Scale Out” when CPU > 80%, and “Scale In” when CPU < 20%). I wanted to know how exactly the cooldowns interact between opposing rules at the engine level.
My tests provided a clear answer. In one scenario, a Scale-Up rule had a 25-minute cooldown, but a Scale-Down action triggered just 6 minutes later.
What Actually Happens: The LastScaleActionTime
By analyzing the AutoscaleEvaluationsLog via Log Analytics, here is exactly what the engine was doing.
The crucial detail to understand is a property in the logs called LastScaleActionTime. The Autoscale engine records a single, unified timestamp whenever any scaling action (in or out) successfully executes within the profile.
When evaluating the rules, the engine checks the time elapsed since this shared LastScaleActionTime. However, it evaluates that time elapsed against the individual cooldown period configured on the specific rule it is currently checking.
Here is how that played out in my tests:
- The Trigger: A Scale-Up action successfully fired. The engine updated the LastScaleActionTime.
- The Next Cycle: A few minutes later, the engine evaluated the metrics again.
- Evaluating Scale-Up: It checked the Scale-Up rule, looked at the LastScaleActionTime, saw the 25-minute cooldown hadn’t expired, and blocked it.
- Evaluating Scale-Down: Crucially, the engine didn’t stop. It moved to evaluate the Scale-Down rule. This rule had its own independent, shorter cooldown. The engine checked the same LastScaleActionTime, but because that specific rule’s timer had elapsed, the evaluation proceeded.
- The Action: The metric condition was met, and the scale-down triggered.
- The LastScaleActionTime was updated for all rules.
The Proof (KQL)
If you are shipping your Autoscale logs to Log Analytics, you can verify this behavior yourself. Look for the transition from a blocked rule to a triggered one within the same CorrelationId:
AutoscaleEvaluationsLog| where OperationName in ("ScaleRuleCooldownEvaluation", "ScaleRuleEvaluation", "InstanceUpdateEvaluation")| project TimeGenerated, OperationName, SkipRuleEvaluationForCooldown, LastScaleActionTime, InstanceUpdateReason, ShouldUpdateInstance, CorrelationId| order by TimeGenerated asc
When you run this query, pay close attention to the OperationName to see exactly where the time counter stands:
- ScaleRuleCooldownEvaluation: This tells you the time counter is still inside the cooldown period for that specific rule. The rule is blocked from proceeding.
- ScaleRuleEvaluation: This event means the time counter is outside the cooldown period. The engine is now actively checking the rule’s metric thresholds to see if it should fire.
Every rule looks at the same LastScaleActionTime, but each rule uses its own cooldown timer to decide if it is allowed to fire. That’s how the Autoscale engine manages capacity for the Service Bus Premium tier where these dynamics are critical.
The Real-World Consequences
Understanding this mechanism is vital because failing to account for it can lead to severe operational issues.
Consider this scenario: You set your scale-out rule to have a long cooldown (e.g., 1 hour) to prevent flapping, but leave your scale-in rule with a short cooldown. A scale-in action occurs as traffic drops. Just 15 minutes later, a massive spike in messages hits your queue.
Because the scale-out rule evaluates against the LastScaleActionTime (which was the scale-in 15 minutes ago), your system is blocked from scaling up for the remaining 45 minutes. You end up with 45 minutes of heavy message spikes hitting a severely under-provisioned Service Bus (fewer Messaging Units than you started with). It gets ugly fast.
Depending on your rule sets, this isolated timer mechanism can introduce unexpected capacity fluctuations and significantly alter how your Service Bus behaves.
A Curious Discrepancy: Does Behavior Vary By Resource?
Moreover, I have observed hints that this autoscale cooldown assessment might actually differ depending on the underlying Azure resource.
While my investigation focused heavily on the Service Bus, the Microsoft reviewer who validated my PR tested the behavior using an App Service Plan. Interestingly, the log events they posted in the updated documentation differ from mine. Their cooldown evaluation log contained an array of scale directions, a property that was completely absent from my Service Bus logs.
This strongly implies that the Autoscale behavior, or at least its log schema, diverges between App Service Plans and Service Bus. I plan to investigate this inconsistency next, along with other resources like Azure Stream Analytics.
But until then, the KQL query provided above remains your best tool to debug and verify the exact scaling behavior for any resource in your architecture.
