Filter concepts by levelShowing all levels.

AWS · Section 37

Monitoring — CloudWatch

Level
intermediate
Read
40 min
Concepts
5

A CloudWatch metric is identified by a namespace, a name, and up to thirty dimensions — and every unique dimension combination is a separate metric, which is why a request id belongs in a log and not in a dimension. You read metrics as statistics over a period chosen at query time, and retention is tiered with automatic rollup: 1-minute data for 15 days, 5-minute for 63 days, 1-hour for 15 months. CloudWatch Logs stores events in streams grouped into log groups, and the log group is the unit that carries retention (never expire by default), access control, metric filters, and subscription filters. A metric filter is the bridge from a log pattern to a metric an alarm can watch. Alarms have three states, act only on sustained state changes, and are the only part of CloudWatch that does anything on its own — dashboards explain, alarms detect. Which signals those alarms should watch is the real design work: latency at a percentile, error rate as a ratio, saturation of whatever resource actually binds the service, queue depth as the early warning, and at least one business KPI, because a system receiving no requests looks perfectly healthy on every infrastructure metric.

What is true here

  1. A metric is namespace + name + dimensions; each dimension combination is its own metric, so cardinality must stay bounded.
  2. Metric retention is tiered with rollup — a dashboard with a fixed short period goes blank on long time ranges.
  3. Log groups carry retention, access, metric filters, and subscriptions; retention defaults to never expire.
  4. Alarms act on sustained state changes; evaluation periods and treat-missing-data matter more than the threshold.
  5. Alarm on latency percentiles, error ratios, saturation and queue depth — plus a business KPI that fails when nothing is arriving.

What you will be able to do

  • Identify a metric correctly and avoid publishing high-cardinality dimensions
  • Set log group retention, and turn a log pattern into an alarmable metric with a metric filter
  • Configure an alarm with sensible evaluation periods and missing-data handling, wired to an SNS action
  • Explain what metrics, logs, traces, and alarms each answer, and pick the right one for a question
  • Choose a signal set that predicts user pain rather than one that is easy to collect
From raw telemetry to an alarm worth waking up for
watched byvia metricfiltersunderstoodthroughapplied as

Metrics, dimensions, statistics

Log groups, metric filters, Insights

Alarms, dashboards, actions

Metrics vs logs vs traces vs alarms

The signals worth alarming on

  • Metrics, dimensions, statistics
    • leads to Alarms, dashboards, actions (watched by)
  • Log groups, metric filters, Insights
    • leads to Alarms, dashboards, actions (via metric filters)
  • Alarms, dashboards, actions
    • leads to Metrics vs logs vs traces vs alarms (understood through)
  • Metrics vs logs vs traces vs alarms
    • leads to The signals worth alarming on (applied as)
  • The signals worth alarming on

Monitoring — CloudWatch

The metric data model, CloudWatch Logs and its filters, alarms and dashboards, and choosing the signals worth alarming on.

Metrics, Dimensions, and Statistics

coreintermediate

A CloudWatch metric is a time-ordered set of data points — one variable you watch over time, such as an EC2 instance's CPU usage. Each metric is identified by a namespace, a name, and up to thirty dimensions (name/value pairs like `InstanceId=i-0abc`). You never read the raw points directly: you ask for a statistic — Average, Sum, Maximum, a percentile — aggregated over a period you choose.

Think of it as

A metric is a labelled measuring tape and the dimensions are the label. Every unique combination of dimensions is its own tape, even when the metric name is the same — which is why you can read `Latency{Server=Prod,Region=Frankfurt}` but not `Latency{Server=Prod}` unless you published that combination too.

What we're doing: Understand why a dashboard graph goes blank when you widen the time range past two weeks.

resolution-rollup.txttext
Widget: custom metric OrderLatency, statistic p99, period 60 seconds.

Last 6 hours   -> a line, one point per minute.
Last 7 days    -> still a line; 1-minute data is kept for 15 days.
Last 30 days   -> empty, because 1-minute data older than 15 days has been
                  rolled up and is only retrievable at 5-minute resolution.

Fix: set the widget's period to 300 for the 30-day view.
1
The period is a read-time choice, so the same stored metric can be graphed at several resolutions — but only at resolutions the data still exists at.
5
Nothing was deleted. The 1-minute series aged out of its 15-day window and the same data now lives at 5-minute granularity.

Why this works: CloudWatch rolls metric data up as it ages rather than keeping every point forever. A dashboard with a hard-coded short period looks broken on long time ranges, and the fix is to match the period to the age of the data rather than to assume the metric stopped being published.

Publishing a custom metric with a new dimension value per request

Wrong

text
# Dimension: RequestId=<uuid>  — one new metric per request

Better

text
# Dimension: Endpoint=/orders, Status=5xx  — a bounded set of values.
# Put the request id in the log line, not in a dimension.

What you see: The metrics bill grows in proportion to traffic, and no graph is usable — every dimension combination has exactly one data point, so there is nothing to aggregate.

Why: Every unique dimension combination is a separate metric, and custom metrics are billed per metric. A high-cardinality dimension therefore turns each request into its own billable time series with a single point — the opposite of what a metric is for. Identifiers belong in logs, where cardinality is free.

What identifies a metric

AWS/ApplicationELB · TargetResponseTime · {LoadBalancer=app/api-lb, TargetGroup=tg-api} · p99 over 60s

AWS/ApplicationELB

Namespace — The container. Metrics in different namespaces never aggregate together.

TargetResponseTime

Metric name — The variable being measured over time.

{LoadBalancer=app/api-lb, TargetGroup=tg-api}

Dimensions — Name/value pairs that are part of the identity — a different combination is a different metric.

p99 over 60s

Statistic + period — How the points are aggregated, and over what window. Chosen at read time, not at publish time.

  • Whole: AWS/ApplicationELB · TargetResponseTime · {LoadBalancer=app/api-lb, TargetGroup=tg-api} · p99 over 60s
  • AWS/ApplicationELB — Namespace: The container. Metrics in different namespaces never aggregate together.
  • TargetResponseTime — Metric name: The variable being measured over time.
  • {LoadBalancer=app/api-lb, TargetGroup=tg-api} — Dimensions: Name/value pairs that are part of the identity — a different combination is a different metric.
  • p99 over 60s — Statistic + period: How the points are aggregated, and over what window. Chosen at read time, not at publish time.

Metric retention and rollup

Metric retention and rollup
Period of the data pointsAvailable forWhat happens next
Under 60 seconds (high resolution)3 hoursRolled up into the 60-second series
60 seconds (1 minute)15 daysRolled up into 5-minute resolution
300 seconds (5 minutes)63 daysRolled up into 1-hour resolution
3600 seconds (1 hour)455 days (15 months)Expires on a rolling basis

Together

text
# Read a statistic, not raw points — namespace + name + dimensions + period
aws cloudwatch get-metric-statistics \
  --namespace AWS/ApplicationELB --metric-name TargetResponseTime \
  --dimensions Name=LoadBalancer,Value=app/api-lb/50dc6c495c0c9188 \
  --start-time 2026-08-29T09:00:00Z --end-time 2026-08-29T10:00:00Z \
  --period 60 --extended-statistics p99

Remember: namespace + name + dimensions identify a metric; every dimension combination is its own metric, so keep cardinality bounded. Statistic and period are read-time choices. Retention is tiered with rollup — 1-minute data for 15 days, 5-minute for 63 days, 1-hour for 15 months.

See also: cloudwatch logs metric filters and insights · alarms dashboards and event driven actions

Log Groups, Metric Filters, and Logs Insights

coreintermediate

CloudWatch Logs stores log events. A log stream is a sequence of events from one source — one container, one function instance, one server. A log group is a set of streams that share retention, monitoring, and access-control settings, and it is the unit you actually configure. Metric filters turn matching log lines into a metric you can alarm on, and Logs Insights is the query language for reading the logs directly.

Think of it as

A log stream is one writer's notebook; a log group is the shelf those notebooks sit on. Settings — how long to keep them, who can read them — belong to the shelf, never to an individual notebook, which is why a per-container retention policy is not a thing you can configure.

What we're doing: Alarm on application errors that exist only as log lines, without shipping logs anywhere else.

metric-filter-to-alarm.txttext
Log group: /aws/ecs/api  (retention set to 30 days)

Metric filter: pattern { $.level = "ERROR" }
  -> metric ApiErrors in namespace Api/Prod, value 1 per match

Alarm: Sum(ApiErrors) >= 5 over 2 consecutive 1-minute periods
  -> SNS topic -> on-call

Nothing leaves CloudWatch Logs. The alarm fires on a pattern in the log
text itself, turned into a numeric series.
1
Retention is set on the group. Left alone it is "never expire", and the storage bill grows forever.
4
The filter pattern here is a JSON field match, which only works because the application logs structured JSON. Free-text matching is brittle by comparison.
7
Two consecutive periods, not one — a single blip should not page anyone. Sum, not Average, because the question is "how many errors", not "how error-ish".

Why this works: Metric filters are the bridge between the two data types: logs are where detail lives, metrics are what alarms work on. Structured JSON logging is what makes the bridge reliable, because the filter matches a named field instead of a substring that a message change can silently break.

Creating log groups without setting retention

Wrong

text
# Create the log group, ship logs, move on

Better

text
aws logs put-retention-policy --log-group-name /aws/ecs/api \
  --retention-in-days 30

What you see: CloudWatch Logs storage becomes one of the larger lines on the bill, made mostly of debug output from services that were decommissioned years ago.

Why: AWS documents the default plainly: log data is stored indefinitely unless you configure a retention setting. Nothing warns you, because indefinite retention is a valid choice — it is only the unchosen default that costs money.

Log group as the unit of configuration

Log group /aws/ecs/api

stream: task/abc123

one container

stream: task/def456

another container

stream: task/ghi789

a third

What the group is wired into

Metric filter

pattern → metric → alarm

Subscription filter

Lambda / Kinesis / Firehose

Logs Insights

query across all streams

  • Log group /aws/ecs/api — retention, access control, metric filters, subscriptions
    • stream: task/abc123 — one container
    • stream: task/def456 — another container
    • stream: task/ghi789 — a third
  • What the group is wired into
    • Metric filter — pattern → metric → alarm
    • Subscription filter — Lambda / Kinesis / Firehose
    • Logs Insights — query across all streams

Reading logs: three different tools

Reading logs: three different tools
ToolUse it whenWhat it produces
Metric filterYou want an alarm on something that only appears in logsA CloudWatch metric, alarmable like any other
Logs InsightsYou are investigating and do not know the query yetAd-hoc query results over a time range
Subscription filterEvents must reach another system as they arriveA near-real-time stream to Lambda / Kinesis / Firehose

Together

text
# Logs Insights: the slowest ten requests in the last hour
fields @timestamp, requestId, durationMs, route
| filter durationMs > 1000
| sort durationMs desc
| limit 10

Remember: Log stream = one source; log group = the shelf, and the unit for retention, access, filters, and subscriptions. Retention defaults to never expire — set it. Metric filters turn log patterns into alarmable metrics; Logs Insights is for investigation; subscription filters stream events onward.

See also: cloudwatch metrics dimensions and statistics · alarms dashboards and event driven actions

Alarms, Dashboards, and Event-Driven Actions

coreintermediate

An alarm watches one metric against a threshold over time and takes an action when the comparison holds. It has three states — OK, ALARM, and INSUFFICIENT_DATA — and it only acts on a sustained state change, not on merely being in a state. The action is a notification to an SNS topic or an Auto Scaling policy, which is what turns monitoring into something that does work rather than something someone has to be watching.

Think of it as

An alarm is a thermostat, not a thermometer. A thermometer shows the temperature; a thermostat has a threshold, a tolerance for how long it must be crossed, and something it switches on when it is. A dashboard is the wall of thermometers you look at afterwards to understand why.

What we're doing: Page on a real availability problem without paging on a single slow minute.

alarm-shape.txttext
Metric: 5xx rate = Sum(HTTPCode_Target_5XX_Count) / Sum(RequestCount)
Period: 60s, evaluation periods: 3, datapoints to alarm: 2 of 3

Threshold: > 1%

Treat missing data: notBreaching — the metric is absent when no requests
arrive at 04:00, and no traffic is not an outage.

Action: SNS -> on-call rotation. OK action: SNS -> the same topic, so the
recovery is announced too.
2
2-of-3 tolerates one bad minute in a three-minute window. A 1-of-1 alarm on a noisy metric produces pages nobody trusts, which is worse than no alarm.
5
This is a design decision, not a default to accept. Getting it wrong is how an alarm pages at 4am every night, or never fires when the service is completely dead.
8
Without an OK action, the on-call has to check manually whether it recovered. The recovery notification is half the value of the alarm.

Why this works: The three knobs that decide whether an alarm is trusted are the statistic, the evaluation window, and the missing-data behaviour. Threshold is the one people tune, and it is usually not the one that is wrong.

Building dashboards instead of alarms

Wrong

text
# A beautiful 24-widget dashboard, no alarms configured

Better

text
# One alarm per user-visible failure mode, wired to SNS; the dashboard is
# for the investigation that follows the page

What you see: Incidents are discovered by customers rather than by the system, and the post-incident review finds the dashboard clearly showed the problem for forty minutes.

Why: A dashboard requires a human to be looking at it. An alarm is the only part of CloudWatch that acts on its own. Dashboards are for understanding a problem you already know about; they are not a detection mechanism.

Alarm states
data arrives,within thresholddata arrives,breachingN evaluation periodsbreaching → actionrecovers →OK actionmetric stopsbeing publishedmetric stopsbeing published

INSUFFICIENT_DATA

start

OK

ALARM

  • INSUFFICIENT_DATA (start)
    • → OK when data arrives, within threshold
    • → ALARM when data arrives, breaching
  • OK
    • → ALARM when N evaluation periods breaching → action
    • → INSUFFICIENT_DATA when metric stops being published
  • ALARM
    • → OK when recovers → OK action
    • → INSUFFICIENT_DATA when metric stops being published

Treat-missing-data choices, and what each one means

Treat-missing-data choices, and what each one means
SettingMissing data is treated asFits when
notBreachingWithin the thresholdThe metric is only published when something happens (e.g. an error count)
breachingBreaching the thresholdSilence itself is the failure — a heartbeat metric
ignoreThe alarm keeps its current stateSparse data where you want the last real signal to persist
missingINSUFFICIENT_DATA (the default behaviour)You want the gap visible rather than interpreted

Together

text
# "No successful health check in 5 minutes" — silence is the failure
aws cloudwatch put-metric-alarm --alarm-name api-heartbeat-missing \
  --namespace Api/Prod --metric-name HealthyChecks --statistic Sum \
  --period 60 --evaluation-periods 5 --threshold 1 \
  --comparison-operator LessThanThreshold --treat-missing-data breaching \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:oncall

Remember: An alarm watches one metric, has states OK / ALARM / INSUFFICIENT_DATA, and acts only on a sustained state change. Set the evaluation periods and the missing-data behaviour deliberately — those, not the threshold, are what make an alarm trustworthy. Dashboards explain; alarms detect.

See also: cloudwatch metrics dimensions and statistics · designing observability signals · event buses rules and targets

Metrics vs Logs vs Traces vs Alarms

standardintermediate

A metric is a number over time — cheap to store, aggregated, no detail about any one request. A log is a record of one event, with full detail and no aggregation. A trace follows one request across every service it touched. An alarm is not data at all: it is a rule over a metric that takes an action. Choosing the wrong one is the most common reason an investigation stalls.

Think of it as

Metrics tell you something is wrong. Traces tell you where. Logs tell you what happened there. Alarms are what wake you up so you start looking. Each one answers exactly one of those questions and none of the others.

text
# The same incident, seen through each type
metric  Sum(ApiErrors) by minute            -> how bad, since when
alarm   Sum(ApiErrors) >= 5 for 2 periods    -> who gets woken up
trace   api 40ms | auth 15ms | db 3,900ms    -> where the time went
log     filter orderId = "88214"             -> what happened to one order
Aggregate or individual

Metrics + alarms

  • +Aggregated numbers over time
  • +Cheap at high traffic, expensive at high cardinality
  • +Answer "is something wrong, and how much"
  • +The only type an alarm can watch

Logs + traces

  • Individual events and individual requests
  • Cheap at high cardinality, expensive at high volume
  • Answer "what happened to this one request"
  • Where the detail an alarm cannot carry lives
  • Metrics + alarms
    • Aggregated numbers over time
    • Cheap at high traffic, expensive at high cardinality
    • Answer "is something wrong, and how much"
    • The only type an alarm can watch
  • Logs + traces
    • Individual events and individual requests
    • Cheap at high cardinality, expensive at high volume
    • Answer "what happened to this one request"
    • Where the detail an alarm cannot carry lives

Reaching for logs to answer an aggregate question

Wrong

text
# Run a Logs Insights query counting ERROR lines every minute to decide
# whether to page

Better

text
# Metric filter turns the ERROR pattern into a metric once; the alarm
# watches the metric

What you see: Detection is slow and expensive — every check scans a growing volume of log data, and the cost rises with traffic rather than with the number of things being monitored.

Why: Logs are stored as individual events, so an aggregate question means scanning them all every time it is asked. A metric is aggregated once on the way in, which is why alarms watch metrics and not logs. The metric filter exists precisely to move a log-shaped signal into metric shape.

Which telemetry type answers which question

Which telemetry type answers which question
QuestionTypeWhy not the others
Is the error rate above 1% right now?Metric (+ alarm)Counting log lines for this on every check is slow and expensive
Why did order 88214 fail?LogA metric has no room for an order id
Where did this slow request spend its 4 seconds?TracePer-service logs show durations but not the causal chain
Should someone be woken up?AlarmOnly alarms take action; the rest are data
How many unique users were affected?Log query (Logs Insights)User id as a metric dimension would create one metric per user

Together

text
# Metric: how bad, and since when
Sum(ApiErrors) by minute

# Trace: where the time went for one slow request
trace_id=1-6890a1b2-... -> api 40ms | auth 15ms | db 3,900ms

# Log: what actually happened there
fields @timestamp, orderId, error | filter orderId = "88214"

Remember: Metrics: aggregated numbers, bounded cardinality, what alarms watch. Logs: individual events, unlimited cardinality, where detail lives. Traces: one request across services. Alarms: a rule with an action, not a data type.

See also: cloudwatch logs metric filters and insights · designing observability signals

Designing Observability Around the Right Signals

coreadvanced

Monitoring every metric a service emits produces noise. Pick a small set of signals that predict user pain — how slow requests are, how many fail, how close a resource is to its limit, how much work is arriving, how much is waiting — and alarm on those. Then add one or two business measures, because a system can be perfectly healthy by every technical measure while no orders are being placed.

Think of it as

Think of a car dashboard. It shows speed, fuel, engine temperature and a small number of warning lights — not the position of every valve. The engineering effort is in choosing which handful of numbers earn a place, and that choice is about what fails, not about what is easy to measure.

What we're doing: Explain a latency incident that CPU and memory graphs showed nothing about.

saturation-first.txttext
14:02 DatabaseConnections on the RDS instance reaches its max_connections
      ceiling. CPU is 30%. Memory is fine.

14:03 Application connection pool: every checkout now waits. Queue depth
      on the request queue starts climbing.

14:05 p99 latency crosses 3s. Average latency is still 210ms, so the
      average-latency dashboard widget looks normal.

14:07 Gateway timeouts. Error rate alarm finally fires — five minutes
      after the saturation signal was already at 100%.
1
Saturation of a non-obvious resource. CPU and memory are the two most-watched signals and neither of them moved.
4
Queue depth is the earliest user-visible symptom, and it is a number, not an error — nothing has failed yet.
7
p99 moves minutes before the average does. An alarm on the average would have fired last, or not at all.

Why this works: Alarming only on errors means being told after users are affected. Saturation signals move first, which is what makes them worth the effort of instrumenting — connection pool usage and queue depth are usually the two that pay for themselves fastest.

Alarming on CPU because it is the metric that comes for free

Wrong

text
# Alarm: CPUUtilization > 80% on every instance

Better

text
# Alarm on the user-visible signals (p99 latency, error rate) and on the
# saturation signal that actually binds this service — often the database
# connection pool, not CPU

What you see: CPU alarms fire during harmless batch jobs and stay quiet during real incidents, so the on-call rotation learns to dismiss them.

Why: CPU is instrumented by default, which makes it the easiest metric to alarm on and not the one that predicts failure. Most web services are bound by an I/O resource — connections, concurrency, a downstream API — and are perfectly capable of failing at 20% CPU.

Signals in the order they usually move

Throughput up

more work arriving

Saturation up

pool, CPU, concurrency near limit

Queue depth up

work waiting, not yet failing

Latency up

p99 first, average later

Errors

timeouts — the user finally notices

  1. Throughput up — more work arriving
  2. Saturation up — pool, CPU, concurrency near limit
  3. Queue depth up — work waiting, not yet failing
  4. Latency up — p99 first, average later
  5. Errors — timeouts — the user finally notices

A starting signal set, and what each one catches

A starting signal set, and what each one catches
SignalMeasured asCatches
Latencyp95 / p99 per endpointSlowness the average hides
Error rate5xx ÷ total requestsFailures, independent of traffic volume
Saturation% of a hard limit (pool, CPU, concurrency)The problem before it becomes latency
ThroughputRequests or messages per minuteTraffic loss, and context for the others
Queue depthMessages visible / oldest message ageA consumer that cannot keep up
Cache hit rateHits ÷ (hits + misses)A cache that silently stopped working

Together

text
# Error rate as a ratio, using metric math rather than a raw count
{
  "Id": "errorRate",
  "Expression": "100 * errors / requests",
  "Label": "5xx %"
}
# with errors = Sum(HTTPCode_Target_5XX_Count), requests = Sum(RequestCount)

Remember: Latency at a percentile, error rate as a ratio, saturation of the resource that actually binds you, throughput for context, queue depth as the early warning — plus at least one business KPI, because a system that receives no requests looks perfectly healthy.

See also: alarms dashboards and event driven actions · metrics logs traces and alarms · connections and database monitoring

Advertisement