Alarms without actions: the basics
What does it mean for a CloudWatch alarm to have no actions?
A CloudWatch alarm watches a metric, evaluates it against a threshold over a window of datapoints, and transitions between three states: OK, ALARM, and INSUFFICIENT_DATA. The transition itself does not page anyone or trigger anything — actions do. Each alarm has three independent action lists: AlarmActions (fired when entering ALARM), OKActions (when recovering to OK), and InsufficientDataActions (when metrics stop flowing). If those lists are empty, the alarm changes state in the AWS console and the alarm history, and nothing else happens.
"Bad" looks like an alarm with AlarmActions: []. It might have a polished name, a sensible threshold, and a useful metric — but when the workload finally misbehaves, the alarm enters ALARM, sits in the console, and waits for someone to randomly notice. There is no SNS publish, no Lambda invocation, no autoscaling step, no SSM Incident Manager response plan. The transition is logged; the human is not.
AWS doesn't surface a built-in check for this — it shows up in third-party audit tools (Trusted Advisor's CloudWatch section, AWS Config custom rules, Security Hub bridge rules) and in any internal compliance check worth its salt. The pattern is one of the most common monitoring failures because creating an alarm without actions is one CLI call shorter than creating one with actions — and the alarm still looks correct in the dashboard until the day you need it.
In this lesson you'll learn why CloudWatch alarms so often end up actionless, how the alarm action model actually works (three independent lists, multiple action types), how to audit your fleet for the pattern in a single CLI call, and how to wire up minimum-viable alerting that scales from a Slack channel to PagerDuty. You'll see a real audit query, a bulk remediation flow, and the prevention pattern that stops actionless alarms from coming back.
The Knight Capital lesson nobody learns from
In 2012 Knight Capital lost $440 million in 45 minutes because a deployment left old code running on one of eight servers. The post-mortem found that alerts had been firing on the system the entire morning — an email-based alarm that nobody on the trading desk read in real time because the inbox was full of low-severity noise. The alarm worked. The action — a single email address — did not. Alarms without actions are the most visible version of this same failure mode: the system is shouting, and there's literally nobody on the other end of the line.
Wiring up an actionless alarm in action
Marco is finishing a quarterly compliance audit for a retail FinOps customer when ALM-003 fires across 14 production alarms. The most obvious offender: an alarm named SainsburysLoadbasedInstanceRunning, watching CPU on a load-based EC2 fleet. Severity HIGH. It's been in OK state for weeks — but every time it has fired ALARM in the last 90 days, no human has been notified.
He starts by confirming the finding: pull the alarm config and check the AlarmActions list. If it's empty, the audit tool is right; if it has stale ARNs, that's a different kind of broken (also worth knowing about, but a separate fix).
Once the actionless state is confirmed, he wires the alarm to the team's existing SNS topic — an inexpensive, idempotent change that flips the alarm from "a logged silence" to "something that pages the on-call rotation."
First, audit the whole account for alarms with no AlarmActions. The JMESPath filter does the work — a single output table makes the scope of the problem obvious.
Every alarm here transitions through ALARM/OK with nobody on the other end.
Now wire the worst offender to the team's SNS topic. put-metric-alarm is idempotent — it replaces the alarm in place, preserving the metric, threshold, and evaluation periods. Only the actions change.
The alarm now publishes to SNS on every ALARM transition — and on recovery via OKActions.
The CloudWatch alarm action model under the hooddeep dive
Every CloudWatch alarm owns three independent action lists. AlarmActions fire on the transition into ALARM, OKActions on the transition back to OK, and InsufficientDataActions when the underlying metric stops reporting datapoints inside the evaluation window. The lists are not symmetric — you can have eight actions on AlarmActions and zero on OKActions, and the alarm is still valid. Most teams forget OKActions entirely, which means on-call gets paged but never gets the "all clear" notification.
Each action is an ARN — and the supported types are wider than people remember. SNS topics (the most common), Auto Scaling policies (arn:aws:autoscaling:...:scalingPolicy/...), EC2 instance actions (arn:aws:automate:region:ec2:stop|terminate|reboot|recover), SSM Incident Manager response plans, and Lambda invocations via SNS-bridged subscriptions. An alarm can have several actions of different types in the same list, so a single transition can both page the on-call and trigger an autoscale step.
The detection pattern for an actionless alarm is a one-line JMESPath query against describe-alarms: filter on length(AlarmActions)==0``. There's no native AWS check that does this for you across the fleet, which is why the pattern hides — the console shows each alarm individually and there's no "sort by actionless" column. AWS Config can fill the gap with a custom rule, and Security Hub can be configured to ingest the result, but neither is on by default.
# The full action-model surface — one alarm can wire all three lists, each with multiple types.
aws cloudwatch put-metric-alarm \
--alarm-name rds-prod-checkout-FreeStorageSpace \
--metric-name FreeStorageSpace \
--namespace AWS/RDS \
--statistic Average \
--period 300 \
--threshold 10737418240 \
--comparison-operator LessThanThreshold \
--evaluation-periods 2 \
--alarm-actions \
arn:aws:sns:eu-west-1:123456789012:db-oncall \
arn:aws:ssm-incidents::123456789012:response-plan/rds-storage-low \
--ok-actions \
arn:aws:sns:eu-west-1:123456789012:db-oncall \
--insufficient-data-actions \
arn:aws:sns:eu-west-1:123456789012:platform-oncall \
--dimensions Name=DBInstanceIdentifier,Value=checkout-prod What is the impact of alarms without actions?
The most direct impact is invisible incidents. Every alarm that fires without an action is an outage, degradation, or budget breach where the system tried to tell you and you didn't get the message. The mean time to detection on an actionless alarm is whatever interval somebody happens to refresh the CloudWatch console — typically hours, sometimes days. Customer-visible failures are usually reported by customers before the team finds them in the dashboard.
The second-order impact is on the team's relationship with monitoring as a whole. When alarms exist that don't page, on-call learns not to trust the dashboard — "yeah, that alarm has been red for three weeks, ignore it." The trust decays alarm by alarm until the whole monitoring layer becomes scenery. New alarms get created with the same actionless default because that's how the existing ones look.
The third-order impact is regulatory. SOC 2 CC7.2, ISO 27001 A.12.4, and PCI DSS Requirement 10 all expect logged events to trigger an investigation or response. An alarm whose history shows repeated ALARM transitions with no documented response is audit evidence that detection exists but response does not — the worst possible combination for a compliance reviewer.
There is no direct AWS bill from an actionless alarm — they cost the same $0.10/month as a wired one — but the cost of every incident that runs longer than it needed to, every customer who churned because of a quietly degraded service, and every audit finding that drags out a recertification all roll up into the total. The fix per alarm is a one-line CLI change. The cost of not fixing it compounds.
How do you fix and prevent actionless alarms?
Wiring actions to alarms is a four-step loop. The inventory-then-wire flow gets the existing fleet healthy; the routing and prevention steps make sure the next batch of alarms is born with actions attached.
1. Inventory every actionless alarm and tag by domain
Run describe-alarms with the actionless filter across every region and every account, then tag each result by owning team — Owner=platform, Owner=data, Owner=app. The tags drive the next step: each team gets a list of alarms they own, and each team already has (or needs to create) a single SNS topic for their on-call rotation.
2. Wire to a tiered alerting topology, not one shared inbox
The anti-pattern is a million alarms pointed at [email protected]. The pattern is two SNS topics per team — team-low (Slack channel) and team-high (PagerDuty/Incident Manager) — with alarm severity deciding which topic the AlarmActions ARN points at. Low severity informs; high severity pages. Mix them and on-call burns out within a quarter.
3. Wire OKActions and InsufficientDataActions, not just AlarmActions
An ALARM-only wiring leaves on-call wondering whether the issue resolved. OKActions pointed at the same low-severity topic close the loop. InsufficientDataActions pointed at the high-severity topic catch the silent killer — the metric stops reporting entirely, often because the agent crashed or the resource was terminated. A complete wiring has all three lists populated.
4. Prevent recurrence with AWS Config or an enforcement Lambda
Deploy an AWS Config custom rule (or a simple Lambda triggered by EventBridge on cloudwatch:PutMetricAlarm) that fails any alarm with an empty AlarmActions list. The rule can be advisory (notify the creator and let it pass) or enforcing (delete the actionless alarm). Most teams start advisory for a quarter, then tighten to enforcing once compliance is high enough that the noise is bearable.
# Inventory + bulk-tag workflow. Each team's SNS topic is the only per-team variable.
aws cloudwatch describe-alarms \
--query 'MetricAlarms[?length(AlarmActions)==`0`].AlarmName' \
--output text \
> actionless-alarms.txt
# For each alarm, look up the owning team via tags and wire the right topic.
while read alarm; do
TEAM=$(aws cloudwatch list-tags-for-resource \
--resource-arn "arn:aws:cloudwatch:eu-west-1:123456789012:alarm:${alarm}" \
--query 'Tags[?Key==`Owner`].Value | [0]' --output text)
TOPIC="arn:aws:sns:eu-west-1:123456789012:${TEAM}-oncall"
aws cloudwatch put-metric-alarm \
--alarm-name "$alarm" \
--alarm-actions "$TOPIC" \
--ok-actions "$TOPIC"
# (other fields preserved from the existing alarm via describe-alarms → jq pipeline)
done < actionless-alarms.txt Quick quiz
Question 1 of 5You've audited the fleet and found 47 CloudWatch alarms with empty AlarmActions lists. What's the right next move?
You scored
0 / 5
Keep learning
Dig deeper into CloudWatch alarm design and the AWS tooling around alerting.
- Amazon CloudWatch alarm actions documentation The full list of supported alarm action ARN types and how to attach them.
- AWS Systems Manager Incident Manager AWS-native response-plan engine that bridges CloudWatch alarms to on-call rotations and runbooks.
- AWS Config custom rules with Lambda The pattern to build a continuous detection rule for actionless CloudWatch alarms across your fleet.
- Google SRE Book — Practical Alerting Canonical guidance on what should and shouldn't page humans — the philosophy behind tiered alerting topologies.
You've completed Fix CloudWatch alarms with no actions. You can now audit a fleet for the actionless pattern, route each alarm to a tiered SNS topology that respects on-call sanity, wire all three action lists for complete coverage, and prevent the pattern from coming back with AWS Config or an enforcement Lambda. The next time a compliance audit fires ALM-003, you'll have a four-step loop ready to run.
Back to the library