Laptop to Production

Alarms that actually fire

Thresholds with reasons, and proving they work.

Time
40 minutes
Level
Beginner

Part 1: The problem

QuickCart, the quiet outage

QuickCart had fourteen alarms. Most of them fired most nights: CPU above 50% during the nightly report, a spike of errors during each deploy. Everyone set up an email rule, and the alarm emails went into a folder.

Then checkout started failing on a Saturday afternoon. The 5xx alarm fired, correctly. Its email went into the folder with the other two hundred. Customers noticed after ten minutes. The team noticed after forty, when someone checked the app store reviews.

The following week, Priya created a new alarm for failed payments. It never sent anything — its email subscription had never been confirmed. Nobody found out until the next incident.

So an alarm has two jobs, and both have to be proved:

  • Fire when something is really wrong — and only then.
  • Reach a person who will act on it.

Part 2: The idea

An alarm watches one metric, compares it with a threshold every period, and does something when the answer changes. Every CloudWatch alarm is in one of three states:

StateMeansWhat you’d see
OKThe metric is on the right side of the thresholdNothing. Quiet is good
ALARMThe metric crossed the threshold for long enoughA notification, if you set one up
INSUFFICIENT_DATANot enough data to decideEvery new alarm starts here. It’s a problem if it stays here

The five decisions in every alarm

DecisionQuickCart’s error alarmWhy it matters
Metric, statistic and periodAppErrorCount, Sum, 1 minuteWhat is being measured, and in what slices
ThresholdGreater than 5Where “fine” ends
Datapoints to alarm1 out of 1How long it has to be bad before anyone is told
Missing dataTreat as goodWhat a silent minute means
ActionEmail the quickcart-alerts topicWho is told, and how

Thresholds need reasons

A threshold picked because it’s a round number fires at the wrong times. Every threshold should answer “why this number?” These are the alarms QuickCart ended up with:

AlarmConditionWhy this threshold
quickcart-unhealthy-hostsUnHealthyHostCount ≥ 1, 2 of 2 minutesAny unhealthy server is worth knowing about. Two minutes avoids firing during a normal deploy
quickcart-alb-5xxHTTPCode_ELB_5XX_Count Sum > 5, 1 of 15xx from the load balancer itself means there’s no healthy server to send to. These should be zero
quickcart-target-5xxHTTPCode_Target_5XX_Count Sum > 10, 2 of 2Errors from the app. A little higher, because the app returns 500 when the database has trouble
quickcart-latency-p95TargetResponseTime p95 > 1 second, 3 of 3A percentile, not an average: an average hides a slow minority
quickcart-cpu-highCPUUtilization Average > 70%, 2 of 2Auto scaling aims for 50%. Staying above 70% means scaling isn’t keeping up
quickcart-db-connectionsDatabaseConnections Maximum > 40, 2 of 2The smallest database sizes allow only a few dozen connections. 40 leaves time to act before it’s full
quickcart-db-storageFreeStorageSpace Minimum under 2 GB, 1 of 1 at 5 minutesA full disk stops the database hard, and recovery is slow
quickcart-app-errorsAppErrorCount Sum > 5, 1 of 1The app’s own view of its errors, from the log metric filter

“M out of N”: how long before it fires

Two settings work together:

SettingMeans
Evaluation periods (N)How many of the most recent periods to look at
Datapoints to alarm (M)How many of those must be bad to fire
SettingFires whenGood for
1 out of 1The latest minute is badThings that should never happen, like load balancer 5xx
2 out of 3Two of the last three minutes are badMost things: ignores a single blip, still quick
3 out of 3Three minutes in a row are badNoisy metrics, like CPU or latency

Asking for more datapoints makes the alarm slower, but calmer. When an alarm keeps switching between OK and ALARM, it’s flapping, and the fix is usually more datapoints or a better threshold.

Missing data is a decision too

Some metrics only report when something happens. The load balancer’s 5xx count, and a metric filter without a default value, publish nothing in a quiet minute. You choose what that silence means:

Treat missing data asSilence counts asUse for
missing (the default)Nothing — the alarm may go to INSUFFICIENT_DATAMetrics that always report, like CPU
good (not breaching)FineError counts: no data means no errors
bad (breaching)A problemHeartbeats: a metric that must always report, so silence means something died
ignoreKeep whatever state it was already inRarely. It can keep a stale ALARM for a long time

Getting it to a person: SNS

An alarm doesn’t send email itself. It publishes a message to an SNS topic, and the topic delivers it to everyone subscribed.

PieceWhat it isQuickCart’s
TopicA named channel that messages are sent toquickcart-alerts
SubscriptionOne destination for that topic: an email address, a phone, a chat tool, another systemThe on-call email address
ConfirmationEmail subscriptions must be confirmed by clicking a link. Until then, nothing is deliveredChecked: Confirmed

One topic can feed many alarms, and many subscribers. Change who gets alerts in one place.

Prove it fires

Key point: An alarm you’ve never seen fire is a guess. Force it red, and watch the notification arrive.

There are two ways to test an alarm, and they test different things:

TestHowWhat it proves
Force the stateaws cloudwatch set-alarm-state. The console has no button for thisEverything after the decision: the topic, the subscription, the email
Cause the real thingMake the metric cross the threshold: send error logs, stop a server, remove a ruleThe metric, the threshold and the datapoints too — the whole chain

Do both. The forced test is quick and safe. The real one is the only proof that the alarm would fire during an actual incident. In QuickCart’s build, removing the database’s security group rule made the app return 500s, and the error alarm fired within two minutes: that’s the evidence it works.

Alarm fatigue

When alarms fire and nothing needs doing, people learn to ignore them. Then they ignore the real one.

RuleIn practice
Every alarm needs an actionIf the answer to “what would I do?” is “nothing”, it’s not an alarm. Put it on a dashboard
Page for customer pain, ticket for everything elseCheckout failing wakes someone. A disk at 70% can wait for the morning
Link every alarm to a runbookPut the link in the alarm’s description, so the person woken up knows the first step
Review what firedEvery alarm that fired without needing action gets retuned or deleted

Composite alarms

When one thing breaks, several alarms often fire together: target 5xx, app errors and latency, all for one database problem. A composite alarm combines other alarms with a rule, and notifies once:

A composite alarm rule
ALARM("quickcart-target-5xx") AND ALARM("quickcart-app-errors")

The individual alarms keep their states for the dashboard, but only the composite one sends the notification. Composite alarms cost a little more than a normal alarm. Start with a few well-tuned alarms and add composite ones when you see the same group firing together.

Part 3: See it

Here’s one metric — errors per minute — and an alarm on it. Pick what the metric does, then change the threshold, the datapoints and the missing-data setting. Watch the state row underneath, and count how many notifications would be sent.

The metric: errors per minute
Threshold: more than
Datapoints to alarm
Treat missing data as
Errors per minute over twelve minutes, with the alarm threshold at more than 5. Shaded minutes show the alarm state.05101502:0002:0202:0402:0602:0802:10threshold: more than 5
Alarm state, minute by minute
  1. 02:00OK
  2. 02:01OK
  3. 02:02OK
  4. 02:03OK
  5. 02:04ALARM
  6. 02:05ALARM
  7. 02:06ALARM
  8. 02:07ALARM
  9. 02:08ALARM
  10. 02:09OK
  11. 02:10OK
  12. 02:11OK
Fired at 02:04: 1 emailThe rule more than 5 errors in the last minute caught the outage and sent one notification. It fired on the first bad minute. Now try the one-minute blip with the same settings.

What to notice: with 1 of 1, the one-minute blip wakes someone up. With 2 of 3 it doesn’t, and the real outage still fires, just a minute later. On the quiet night, the only thing that changes the result is what missing data means.

Part 4: Do it

Pennies — one alarm (about $0.10 a month, charged by the hour, and the free tier covers ten), an SNS topic whose first 1,000 emails a month are free, and a few kilobytes of logs.

You’ll build an error alarm on a log metric, send it to your email, force it to fire, then make it fire for real. About 30 minutes.

You need the AWS CLI with a working login, and the console’s region set to N. Virginia (us-east-1).

  1. Create the log group, stream and metric filter

    If you kept /quickcart/orders and its metric filter from Logs and metrics, skip to the next step.

    In the AWS console: CloudWatchLog groupsCreate log group

    FieldEnter
    Log group name/quickcart/orders
    Retention setting1 week

    Choose Create. Open the new group, and on Log streams choose Create log stream:

    FieldEnter
    Log stream namelab-1

    Then on Metric filters, choose Create metric filter:

    FieldEnter
    Filter pattern{ $.level = "ERROR" }
    Filter namequickcart-app-errors
    Metric namespaceQuickCart/App
    Metric nameAppErrorCount
    Metric value1
    Default value0

    Choose Next between the pages, then Create metric filter.

    You should see

    The log group with one stream, lab-1, and one metric filter.

  2. Create the SNS topic

    In the AWS console: Simple Notification ServiceTopicsCreate topic

    FieldEnter
    TypeStandard
    Namequickcart-alerts

    Leave the rest and choose Create topic.

    You should see

    The topic’s page, with its ARN: arn:aws:sns:us-east-1:111122223333:quickcart-alerts, with your own account number.

  3. Subscribe your email, and confirm it

    On the topic’s page, choose Create subscription.

    FieldEnter
    Topic ARNalready filled in
    ProtocolEmail
    Endpointyour own email address

    Choose Create subscription. Then open your email, find the message from AWS Notifications — it may be in spam — and choose Confirm subscription.

    You should see

    Back in SNS → Subscriptions, refresh: the status is Confirmed. If it still says Pending confirmation, nothing will reach you. Don’t go on until it says Confirmed.

  4. Send some normal traffic, so the metric exists

    The console can only put an alarm on a metric that has data. Normal log lines make the metric filter publish its default value, 0.

    The console has no way to send log lines by hand, so this uses the CLI. First get the current time:

    Terminal
    date +%s
    PartWhat it means
    dateprint the date and time
    +%sas seconds since 1 January 1970
    You should see
    1790349700

    Yours will be larger. Add three zeros to turn it into milliseconds. Create normal.json, putting your number in place of the highlighted one:

    normal.json
    [
      {"timestamp": 1790349700000, "message": "{\"level\":\"INFO\",\"route\":\"/orders\",\"status\":200,\"latency_ms\":41,\"instance_id\":\"i-0aa11\"}"},
      {"timestamp": 1790349700000, "message": "{\"level\":\"INFO\",\"route\":\"/orders\",\"status\":200,\"latency_ms\":38,\"instance_id\":\"i-0bb22\"}"}
    ]

    From the same folder, send it:

    Terminal
    aws logs put-log-events --region us-east-1 --log-group-name /quickcart/orders --log-stream-name lab-1 --log-events file://normal.json
    PartWhat it means
    aws logs put-log-eventssend log events to CloudWatch Logs
    --region us-east-1the region the log group is in
    --log-group-name /quickcart/orderswhich log group
    --log-stream-name lab-1which stream
    --log-events file://normal.jsonread the events from this file
    You should see

    A short JSON reply, without rejectedLogEventsInfo. Wait two or three minutes before the next step, so the metric appears.

  5. Create the alarm

    In the AWS console: CloudWatchAlarmsAll alarmsCreate alarm

    Choose Select metric, then QuickCart/App → Metrics with no dimensions, tick AppErrorCount, and choose Select metric.

    FieldEnter
    StatisticSum
    Period1 minute
    Threshold typeStatic
    Whenever AppErrorCount isGreater
    than5

    Open Additional configuration:

    FieldEnter
    Datapoints to alarm1 out of 1
    Missing data treatmentTreat missing data as good (not breaching threshold)

    Choose Next.

    FieldEnter
    Alarm state triggerIn alarm
    Send a notification to the following SNS topicSelect an existing SNS topic, then quickcart-alerts

    Choose Next.

    FieldEnter
    Alarm namequickcart-app-errors
    Alarm descriptionMore than 5 app errors in a minute. Runbook: https://example.com/runbooks/checkout-5xx

    Choose Next, check the summary, and choose Create alarm.

    You should see

    quickcart-app-errors in the list. It starts as Insufficient data, and within a minute or two changes to OK.

  6. Force it to fire

    The console has no way to set an alarm’s state, so this uses the CLI:

    Terminal
    aws cloudwatch set-alarm-state --region us-east-1 --alarm-name quickcart-app-errors --state-value ALARM --state-reason "Testing the notification path"
    PartWhat it means
    aws cloudwatch set-alarm-statechange an alarm’s state by hand, for testing
    --region us-east-1where the alarm is
    --alarm-name quickcart-app-errorswhich alarm
    --state-value ALARMthe state to put it in
    --state-reason "Testing the notification path"a note that shows in the alarm’s history, so others know it was a test

    It prints nothing when it works.

    You should see

    Within a minute, an email with the subject ALARM: "quickcart-app-errors" in US East (N. Virginia). In the console the alarm shows In alarm, then goes back to OK at its next evaluation, about a minute later, because the metric itself is fine.

  7. Make it fire for real

    Now make the metric itself cross the threshold: eight error lines in one minute is more than 5. Run date +%s again for a fresh time — the alarm only looks at the last minute or so, so the timestamp must be now. Create errors.json with your new number:

    errors.json
    [
      {"timestamp": 1790350000000, "message": "{\"level\":\"ERROR\",\"route\":\"/orders\",\"status\":500,\"latency_ms\":3004,\"error_type\":\"OperationalError\",\"instance_id\":\"i-0aa11\"}"},
      {"timestamp": 1790350000000, "message": "{\"level\":\"ERROR\",\"route\":\"/orders\",\"status\":500,\"latency_ms\":3004,\"error_type\":\"OperationalError\",\"instance_id\":\"i-0bb22\"}"},
      {"timestamp": 1790350000000, "message": "{\"level\":\"ERROR\",\"route\":\"/orders\",\"status\":500,\"latency_ms\":3004,\"error_type\":\"OperationalError\",\"instance_id\":\"i-0aa11\"}"},
      {"timestamp": 1790350000000, "message": "{\"level\":\"ERROR\",\"route\":\"/orders\",\"status\":500,\"latency_ms\":3004,\"error_type\":\"OperationalError\",\"instance_id\":\"i-0bb22\"}"},
      {"timestamp": 1790350000000, "message": "{\"level\":\"ERROR\",\"route\":\"/orders\",\"status\":500,\"latency_ms\":3004,\"error_type\":\"OperationalError\",\"instance_id\":\"i-0aa11\"}"},
      {"timestamp": 1790350000000, "message": "{\"level\":\"ERROR\",\"route\":\"/orders\",\"status\":500,\"latency_ms\":3004,\"error_type\":\"OperationalError\",\"instance_id\":\"i-0bb22\"}"},
      {"timestamp": 1790350000000, "message": "{\"level\":\"ERROR\",\"route\":\"/orders\",\"status\":500,\"latency_ms\":3004,\"error_type\":\"OperationalError\",\"instance_id\":\"i-0aa11\"}"},
      {"timestamp": 1790350000000, "message": "{\"level\":\"ERROR\",\"route\":\"/orders\",\"status\":500,\"latency_ms\":3004,\"error_type\":\"OperationalError\",\"instance_id\":\"i-0bb22\"}"}
    ]

    Send it straight away:

    Terminal
    aws logs put-log-events --region us-east-1 --log-group-name /quickcart/orders --log-stream-name lab-1 --log-events file://errors.json
    PartWhat it means
    aws logs put-log-eventssend log events to CloudWatch Logs
    --region us-east-1the region the log group is in
    --log-group-name /quickcart/orderswhich log group
    --log-stream-name lab-1which stream
    --log-events file://errors.jsonread the eight error lines from this file
    You should see

    Within about two or three minutes, the alarm goes to In alarm and a second email arrives. This time the email’s reason names the real datapoint — something like Threshold Crossed: 1 out of the last 1 datapoints [8.0 (25/09/26 15:26:00)] was greater than the threshold (5.0), with your own time. A few minutes later, with no more errors, the alarm returns to OK.

    If nothing happens after five minutes, the timestamp was probably too old by the time you sent it. Run date +%s, update the file and send it again.

  8. Read the alarm's history

    In the AWS console: CloudWatchAlarmsAll alarmsquickcart-app-errorsHistory

    You should see

    Each state change with its time and reason: the forced one says Testing the notification path; the real one names the datapoint that crossed the threshold. Each Action row shows the notification being sent to quickcart-alerts. This history is your evidence the alarm works.

  9. Clean-up step: Clean up

    Delete the alarm:

    In the AWS console: CloudWatchAlarmsAll alarmsquickcart-app-errorsActionsDelete

    Delete the topic, which removes its subscription too:

    In the AWS console: Simple Notification ServiceTopicsquickcart-alertsDelete

    Type delete me when asked, and choose Delete. Then delete the log group:

    In the AWS console: CloudWatchLog groups/quickcart/ordersActionsDelete log group(s)

    You should see

    No alarms, no quickcart-alerts topic and no /quickcart/orders log group left. The subscription may linger for a short time in the list as it’s removed.

Part 5: Check yourself

An alarm uses "3 out of 3" datapoints with a 1-minute period. Errors spike for one minute, then stop. What happens?

An alarm on an error-count metric sits in INSUFFICIENT_DATA every night. What is the most likely fix?

The alarm shows ALARM in the console, but no email ever arrives. What should you check first?

What does aws cloudwatch set-alarm-state prove?

The team gets 40 alarm emails a week and most need no action. What is the right response?

Part 6: Cheat sheet

On one card
Term or settingIn one line
OK / ALARM / INSUFFICIENT_DATAFine / crossed the threshold long enough / can’t tell
ThresholdWhere “fine” ends. Write down why you chose it
M out of NM bad datapoints in the last N periods fires it
FlappingSwitching between OK and ALARM. Needs more datapoints or a better threshold
Missing data: goodError counts. No data means no errors
Missing data: badHeartbeats. Silence means something died
SNS topicWhere alarms send notifications. Subscriptions deliver them
Pending confirmationNothing is delivered until the email link is clicked
set-alarm-state --state-value ALARMTests the notification path. CLI only
Real triggerThe only proof it fires in an incident
Composite alarmSeveral alarms, one rule, one notification
Alarm fatigueEvery alarm must mean “do something now”

Finished the build and the quiz? Mark it done to fill in its stop on the route map.