The outage happened, the metric spiked, and nobody was paged. Or the alarm you created last week has shown INSUFFICIENT_DATA ever since. Both come down to a short list of causes: the alarm is watching a metric that doesn't exist exactly as written, the period doesn't match how often data arrives, missing data is treated as "fine", or the alarm fired and the notification went nowhere.
Work through the checks in order. Set these first:
export AWS_REGION=eu-west-1
ALARM="prod-api-5xx"
Symptom to cause
| Symptom | Likely cause | Fix |
|---|---|---|
INSUFFICIENT_DATA since creation | Dimension name/value, namespace or region doesn't match a real metric | Copy dimensions from list-metrics |
INSUFFICIENT_DATA on and off | Period shorter than the metric's publish interval, or sparse metric | Raise period, or set TreatMissingData |
Stays OK during an outage | Missing data treated as notBreaching, the wrong statistic, or the source stopped sending | Pick the right statistic; alarm on the absence of data |
Goes to ALARM, no page | ActionsEnabled false, no action for that state, SNS subscription unconfirmed, topic or KMS policy blocks CloudWatch | Check history for action failures |
| Fires late | High EvaluationPeriods, or metric published with delay | Use M-of-N with DatapointsToAlarm |
1. Read what the alarm actually watches
aws cloudwatch describe-alarms --alarm-names "$ALARM" \
--query "MetricAlarms[0].{ns:Namespace,metric:MetricName,dims:Dimensions,stat:Statistic,ext:ExtendedStatistic,period:Period,evals:EvaluationPeriods,dta:DatapointsToAlarm,missing:TreatMissingData,threshold:Threshold,op:ComparisonOperator,actionsEnabled:ActionsEnabled,alarmActions:AlarmActions,state:StateValue,reason:StateReason}"
StateReason often tells you directly. "Insufficient Data: 0 datapoints were received" means the alarm found nothing to evaluate at all. That is almost always a check 2 problem.
List every alarm currently in that state:
aws cloudwatch describe-alarms --state-value INSUFFICIENT_DATA \
--query "MetricAlarms[].[AlarmName,Namespace,MetricName]" --output table
2. Confirm the metric exists with exactly those dimensions
A CloudWatch metric is identified by namespace, name and the complete set of dimensions. An alarm with LoadBalancer only won't match a metric published with LoadBalancer + TargetGroup. Values are case-sensitive, and for ALB the value is app/name/id, not the ARN or the DNS name.
aws cloudwatch list-metrics --namespace AWS/ApplicationELB \
--metric-name HTTPCode_Target_5XX_Count \
--query "Metrics[].Dimensions" --output json
Compare the output with dims from check 1, character for character. The usual mismatches:
- ALB/NLB: ARN pasted instead of the
app/...ornet/...suffix. - EC2 via the CloudWatch agent: the agent adds
InstanceId,ImageId,InstanceType, orpath,deviceandfstypefor disk metrics. The alarm has to include every one of them, or you need to changeappend_dimensions/aggregation_dimensionsin the agent config. - Auto Scaling replaced the instance: an alarm on
InstanceId=i-oldgoes toINSUFFICIENT_DATAfor good. Alarm onAutoScalingGroupName, or use a Metrics Insights query alarm. - Wrong region: alarms evaluate metrics in their own region. Billing metrics (
AWS/Billing) are only inus-east-1. CloudFront metrics are also inus-east-1.
Tip
list-metricsreturns only metrics that received data in roughly the last two weeks. An empty result means either the dimensions are wrong or nothing is being published. Check the source before you edit the alarm again.
3. Pull the raw datapoints at the alarm's period
aws cloudwatch get-metric-statistics --namespace AWS/ApplicationELB \
--metric-name HTTPCode_Target_5XX_Count \
--dimensions Name=LoadBalancer,Value=app/prod-api/0123456789abcdef \
--start-time "$(date -u -d '-3 hours' +%FT%TZ)" --end-time "$(date -u +%FT%TZ)" \
--period 60 --statistics Sum --output table
Look for gaps. Many AWS metrics are only published when something happens. HTTPCode_Target_5XX_Count has no datapoint in a minute with zero 5xx, and Lambda Errors has none when nothing was invoked. Other metrics arrive every 5 minutes (basic EC2 monitoring) or once a day (S3 BucketSizeBytes). A 60-second alarm on a 5-minute metric shows INSUFFICIENT_DATA four minutes out of five.
| Metric pattern | Period | TreatMissingData |
|---|---|---|
| Count of bad events (5xx, errors, throttles) | 60–300 s | notBreaching |
| Continuous gauge (CPU, queue depth) | match publish interval | missing or breaching |
| Heartbeat / "should always report" | 60–300 s | breaching |
| Daily metrics (S3 storage) | 86400 s | missing |
4. Check how missing data and the statistic are handled
TreatMissingData has four values: missing (the default; the alarm can drift to INSUFFICIENT_DATA), notBreaching, breaching and ignore (keep the current state).
Warning
notBreachingon a metric that stops reporting during an outage hides the outage. If the instance, agent or app dies, a CPU or custom heartbeat metric just stops, and the alarm reportsOK. For anything that must always report, usebreaching, or add a second alarm on the source's health (for exampleStatusCheckFailed).
Check the statistic too. Average of a 0/1 error metric over 5 minutes hides a single failure, and Sum of a latency metric means nothing. For latency, use a percentile (p99). For percentile alarms on low-traffic metrics, decide what EvaluateLowSampleCountPercentile should do (evaluate or ignore).
Fix an alarm in place. put-metric-alarm replaces the whole definition, so pass every field again:
aws cloudwatch put-metric-alarm --alarm-name "$ALARM" \
--namespace AWS/ApplicationELB --metric-name HTTPCode_Target_5XX_Count \
--dimensions Name=LoadBalancer,Value=app/prod-api/0123456789abcdef \
--statistic Sum --period 60 --evaluation-periods 5 --datapoints-to-alarm 3 \
--threshold 20 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:oncall \
--ok-actions arn:aws:sns:eu-west-1:111122223333:oncall
5. The alarm fired, but nobody was told
Read the history. Action failures are recorded as Action items:
aws cloudwatch describe-alarm-history --alarm-name "$ALARM" \
--history-item-type Action --max-records 20 \
--query "AlarmHistoryItems[].[Timestamp,HistorySummary]" --output table
aws cloudwatch describe-alarm-history --alarm-name "$ALARM" \
--history-item-type StateUpdate --max-records 20 \
--query "AlarmHistoryItems[].[Timestamp,HistorySummary]" --output table
If there are state changes but no action entries, check ActionsEnabled and whether an action is set for that state. --alarm-actions covers only ALARM. A disabled alarm still changes state; it just doesn't act.
aws cloudwatch enable-alarm-actions --alarm-names "$ALARM"
Then check the SNS side:
TOPIC=arn:aws:sns:eu-west-1:111122223333:oncall
aws sns list-subscriptions-by-topic --topic-arn $TOPIC \
--query "Subscriptions[].[Protocol,Endpoint,SubscriptionArn]" --output table
aws sns get-topic-attributes --topic-arn $TOPIC \
--query "Attributes.{policy:Policy,kms:KmsMasterKeyId}"
SubscriptionArnofPendingConfirmationmeans the email was never confirmed.- The topic policy must let
cloudwatch.amazonaws.comcallsns:Publishif someone replaced the default policy. - If the topic is encrypted with
alias/aws/sns, CloudWatch can't publish to it. Use a customer managed key whose key policy allowscloudwatch.amazonaws.comto usekms:Decryptandkms:GenerateDataKey*. - The topic must be in the same region as the alarm, unless you have deliberately set up cross-account or cross-region delivery.
6. Test end to end
Force a state change. The alarm returns to its real state at the next evaluation:
aws cloudwatch set-alarm-state --alarm-name "$ALARM" \
--state-value ALARM --state-reason "Paging test by on-call"
Caution
set-alarm-statetriggers every action on the alarm, including Auto Scaling policies, EC2 stop/terminate/reboot actions and Systems Manager automation. CheckAlarmActionsbefore you test on a production alarm that does more than notify.
Prevent it next time
- Build alarms from
list-metricsoutput, not from the console's free-text fields or memory. - Alarm on stable dimensions (
AutoScalingGroupName,TargetGroup,ServiceName), not instance IDs. - Give each alarm a
TreatMissingDatathat you have chosen on purpose, and add a heartbeat alarm withbreachingfor every critical data source. - Run a weekly report of
describe-alarms --state-value INSUFFICIENT_DATAand fix or delete what it finds. - Put
set-alarm-statepaging tests into your on-call handover for alarms that only send notifications.