AWS SQS monitoring: the one metric that matters more than queue depth
Queue depth feels like the obvious thing to alarm on for SQS — a growing number of visible messages looks like a problem. But depth spikes constantly for entirely normal reasons: a producer flushes a batch, traffic has a burst, a scheduled job enqueues a day's worth of work at once. None of that is an incident. The metric that actually tells you when something is wrong is ApproximateAgeOfOldestMessage — how long the single oldest message in the queue has been waiting.
What are the 3 metrics that matter for SQS monitoring?
ApproximateAgeOfOldestMessage (processing lag — the primary alarm), ApproximateNumberOfMessagesVisible (queue depth — context, not a primary alarm), and the dead-letter queue's own ApproximateNumberOfMessagesVisible (poison-pill detection). Age tells you if consumers are keeping up regardless of volume; depth tells you how much backlog exists; DLQ depth tells you if specific messages are failing repeatedly rather than just queuing up.
| Metric | What it measures | Role |
|---|---|---|
| ApproximateAgeOfOldestMessage | Seconds since the oldest non-deleted message was sent | Primary alarm — directly threatens data loss as it approaches your retention period |
| ApproximateNumberOfMessagesVisible | Messages available to be received right now | Context — normal to spike; alarm on sustained growth, not absolute count |
| ApproximateNumberOfMessagesNotVisible | Messages currently in flight (received, visibility timeout running) | Diagnostic — high and flat means consumers received messages but aren't deleting them |
| DLQ: ApproximateNumberOfMessagesVisible | Messages that exhausted maxReceiveCount and moved to the dead-letter queue | Poison-pill alarm — anything above zero warrants investigation |
Why age, not depth, is the right primary alarm
Depth answers "how much work is queued." Age answers "is anything actually stuck." A queue can have 50,000 visible messages and be completely healthy if consumers are churning through them within seconds of arrival — high depth, low age. A queue can have 20 visible messages and be in genuine crisis if consumers have been down for an hour — low depth, high age. Age is the metric that correlates with real user impact and real data-loss risk; depth on its own does neither.
Setting the actual alarm threshold
Tie the threshold to your queue's MessageRetentionPeriod, not a flat number — a threshold that makes sense at the 4-day default is meaningless if you've extended retention to 14 days. Use two tiers: an early-warning alarm at 50% of retention (time to investigate before anything is at risk) and a critical alarm at 80% (messages will start expiring soon without intervention).
| Retention period | Warning threshold (50%) | Critical threshold (80%) |
|---|---|---|
| 4 days (default) | 2 days (172,800s) | 3.2 days (276,480s) |
| 7 days | 3.5 days (302,400s) | 5.6 days (483,840s) |
| 14 days (maximum) | 7 days (604,800s) | 11.2 days (967,680s) |
SqsOldestMessageCritical:
Type: AWS::CloudWatch::Alarm
Properties:
AlarmName: sqs-oldest-message-critical
AlarmDescription: >-
Oldest message age is at 80% of retention period — messages will
start expiring and being permanently deleted without intervention.
Namespace: AWS/SQS
MetricName: ApproximateAgeOfOldestMessage
Dimensions:
- Name: QueueName
Value: !GetAtt MyQueue.QueueName
Statistic: Maximum
Period: 60
EvaluationPeriods: 3
Threshold: 276480 # 80% of 4-day (345600s) default retention
ComparisonOperator: GreaterThanThreshold
TreatMissingData: notBreachingDiagnosing consumer failures with Logs Insights
SQS itself doesn't emit per-message processing logs — the useful signal lives in your consumer's logs. For a Lambda consumer, query the function's log group for errors correlated with the time window when age started climbing.
fields @timestamp, @message
| filter @message like /ERROR|Task timed out|Rate Exceeded/
| stats count() as errorCount by bin(5m)
| sort errorCount desc"Rate Exceeded" in Lambda logs while consuming from SQS usually means the function is being throttled — check ConcurrentExecutions against your account or reserved concurrency limit. "Task timed out" errors clustering around one message body pattern usually means a poison pill, not a capacity problem.
The three SQS failure modes, and how to tell them apart
- Poison pill — one message fails processing repeatedly, gets returned to the queue after each visibility timeout, and eventually moves to the DLQ after maxReceiveCount attempts. Tell: DLQ ApproximateNumberOfMessagesVisible rises while the main queue's age stays roughly flat — everything else is processing normally.
- Consumer stopped — no messages are being deleted at all while messages continue arriving. Tell: NumberOfMessagesDeleted flatlines at zero while ApproximateNumberOfMessagesVisible climbs steadily. This is the pattern the consumer-stopped metric math alarm is built to catch directly.
- Consumer under-provisioned — messages are being processed, just slower than they're arriving. Tell: NumberOfMessagesDeleted is greater than zero and roughly steady, but ApproximateAgeOfOldestMessage climbs gradually rather than spiking. This needs more consumer throughput (higher Lambda concurrency, more ECS tasks), not a bug fix.
Related reading
Frequently asked questions
Frequently asked questions
What is ApproximateAgeOfOldestMessage and why does it matter more than queue depth?
It's the age, in seconds, of the oldest message still in an SQS queue that hasn't been deleted. It matters more than depth because it directly measures whether consumers are keeping up with arrivals, regardless of volume — a queue can have high depth and be healthy (fast consumers, big batch), or low depth and be in crisis (consumers down, a handful of stuck messages aging out).
What's a good CloudWatch alarm threshold for SQS message age?
Set it as a percentage of your queue's MessageRetentionPeriod rather than a flat number: a 50% threshold as an early warning, and 80% as critical. For the default 4-day (345,600 second) retention, that's roughly 2 days for warning and 3.2 days for critical. If you extend retention to 14 days, the same percentages scale up proportionally.
How do I know if messages are being lost from an SQS queue?
Messages are silently and permanently deleted once they exceed the queue's MessageRetentionPeriod — there's no separate 'message expired' event to alarm on directly. The only reliable early warning is alarming on ApproximateAgeOfOldestMessage before it reaches the retention period, since once a message expires it cannot be recovered.
What causes SQS poison-pill messages, and how do I detect them?
A poison-pill message is one that fails processing every time it's received — a malformed payload, an edge case your consumer code doesn't handle, or a downstream dependency that will never succeed for that specific message. After maxReceiveCount failed attempts, SQS moves it to the configured dead-letter queue. Detect it by alarming on the DLQ's own ApproximateNumberOfMessagesVisible metric — any sustained value above zero warrants investigation.
Does SQS log every message to CloudWatch Logs?
No. SQS does not emit per-message processing logs to CloudWatch Logs by default — its CloudWatch metrics (queue depth, age, sent/received/deleted counts) are aggregate statistics, not individual message logs. To diagnose why a specific message failed, check your consumer's logs (Lambda function logs, ECS task logs) rather than looking for SQS-side logging, since SQS itself doesn't retain that detail.
Related reading
Still debugging incidents manually?
Nuberio does this automatically — root cause in under 60 seconds, delivered to WhatsApp or Slack.