Guide · AWS SNS
SNS Subscription DLQ and Delivery Retry for MCP Servers — Per-Subscription Dead-Letter Queues, Exponential Backoff
SNS delivery can fail silently — an HTTP subscriber that returns a 5xx, a Lambda that throttles, or an SQS queue with a misconfigured access policy will cause SNS to retry delivery and eventually discard the message with no visible error on the publisher side. SNS dead-letter queues (DLQ) at the subscription level capture messages that exhaust all delivery retries, providing a last-resort recovery path. Unlike SQS DLQs which are attached to queues and capture consumer failures, SNS subscription DLQs are attached to individual subscriptions and capture delivery failures — messages that SNS itself could not route to the subscriber endpoint. For MCP monitoring pipelines, this distinction matters: a message in an SNS subscription DLQ means the delivery infrastructure has a problem (endpoint unavailable, access denied), not the consumer logic.
TL;DR
Set a RedrivePolicy on the SNS subscription with the ARN of an SQS queue as the DLQ. The SQS queue's resource policy must grant sqs:SendMessage to sns.amazonaws.com with the subscription ARN as the source. Configure a custom DeliveryPolicy to control retry count and backoff. Enable delivery status logging via SuccessFeedbackRoleArn and FailureFeedbackRoleArn on the topic to see per-delivery outcomes in CloudWatch Logs.
SNS subscription DLQ vs SQS queue DLQ
| Property | SNS subscription DLQ | SQS queue DLQ |
|---|---|---|
| Captures | Messages SNS could not deliver to the subscriber endpoint after all retries | Messages the SQS consumer could not successfully process after maxReceiveCount receives |
| Configured on | The SNS subscription (RedrivePolicy subscription attribute) | The source SQS queue (RedrivePolicy queue attribute) |
| Failure type | Delivery infrastructure failure (endpoint down, permissions, throttle) | Consumer logic failure (exception, timeout, bad data) |
| Message content | Original SNS message payload plus delivery error metadata | Original SQS message as received by the consumer |
| Trigger | SNS retry policy exhausted (all retry attempts failed) | SQS ApproximateReceiveCount exceeds maxReceiveCount |
For an SNS→SQS fan-out pipeline, you need both: the SNS subscription DLQ catches messages that SNS could not write to the SQS queue; the SQS DLQ catches messages that were delivered to the queue but the Lambda consumer failed to process. An SQS queue with a resource policy error would produce SNS subscription DLQ entries; a consumer bug would produce SQS DLQ entries.
SNS delivery retry policy phases
SNS retries failed deliveries in four sequential phases. The default policy totals 100,015 attempts over 23 days — far too many retries for most MCP monitoring workloads where stale events are useless. Configure a custom policy to fail fast and route to DLQ.
| Phase | Parameter | Default | Description |
|---|---|---|---|
| Immediate retries | numNoDelayRetries | 0 | Retry immediately, no delay |
| Pre-backoff phase | numMinDelayRetries | 0 | Retry with minimum delay (minDelayTarget seconds) |
| Exponential backoff | numRetries (total) minus previous phases | 3 | Linear or exponential backoff between minDelayTarget and maxDelayTarget |
| Post-backoff phase | numMaxDelayRetries | 100,000 | Retry with maximum delay (maxDelayTarget seconds) |
| Parameter | Default value | Description |
|---|---|---|
minDelayTarget | 20 (seconds) | Minimum delay between retries (floor of backoff range) |
maxDelayTarget | 20 (seconds) | Maximum delay between retries (ceiling of backoff range) |
numRetries | 3 | Total retry count across all phases |
numNoDelayRetries | 0 | Immediate retries before any delay |
numMinDelayRetries | 0 | Retries at minimum delay before backoff |
numMaxDelayRetries | 100,000 | Retries at maximum delay after backoff |
backoffFunction | linear | linear, arithmetic, geometric, or exponential |
Recommended policy for MCP monitoring (fast-fail): 3 immediate retries, 2 at 5-second delay, 3 at exponential backoff up to 60 seconds, 0 post-backoff retries. Exhaustion (8 retries total) routes to DLQ within ~3 minutes. A probe result that cannot be delivered within 3 minutes is stale for alerting purposes.
Configuring a subscription DLQ
// Step 1: Create DLQ for SNS subscription failures
const snsDlq = new sqs.Queue(this, "SnsDeliveryDlq", {
queueName: "sns-delivery-failures",
retentionPeriod: Duration.days(14),
});
// Step 2: Add resource policy to allow SNS to write to the DLQ
// Note: the Source must be the SUBSCRIPTION ARN, not the topic ARN
// Use a wildcard for the source when the subscription ARN is not yet known (created after topic)
snsDlq.addToResourcePolicy(new iam.PolicyStatement({
effect: iam.Effect.ALLOW,
principals: [new iam.ServicePrincipal("sns.amazonaws.com")],
actions: ["sqs:SendMessage"],
resources: [snsDlq.queueArn],
conditions: {
ArnLike: {
"aws:SourceArn": `arn:aws:sns:${Stack.of(this).region}:${Stack.of(this).account}:*`,
},
},
}));
// Step 3: Configure subscription with RedrivePolicy (via CfnSubscription for DLQ support)
// CDK L2 SqsSubscription does not directly expose RedrivePolicy — use escape hatch
const subscription = new sns_subscriptions.SqsSubscription(myQueue, {
rawMessageDelivery: true,
});
const cfnSubscription = topic.addSubscription(subscription).node
.defaultChild as sns.CfnSubscription;
cfnSubscription.redrivePolicy = {
deadLetterTargetArn: snsDlq.queueArn,
};
CDK escape hatch note: As of CDK v2, the SqsSubscription construct exposes deadLetterQueue as a prop — use that directly instead of the escape hatch above if available in your CDK version:
// CDK v2.x — preferred approach
topic.addSubscription(
new sns_subscriptions.SqsSubscription(myQueue, {
rawMessageDelivery: true,
deadLetterQueue: snsDlq, // sets RedrivePolicy automatically
})
);
Custom delivery retry policy for fast-fail
// Set custom delivery policy on the subscription via CLI (CDK L2 does not expose DeliveryPolicy)
aws sns set-subscription-attributes \
--subscription-arn arn:aws:sns:us-east-1:123456789012:mcp-probe-results:uuid \
--attribute-name DeliveryPolicy \
--attribute-value '{
"healthyRetryPolicy": {
"numRetries": 8,
"numNoDelayRetries": 3,
"numMinDelayRetries": 2,
"numMaxDelayRetries": 0,
"minDelayTarget": 5,
"maxDelayTarget": 60,
"backoffFunction": "exponential"
}
}'
# Verify current policy
aws sns get-subscription-attributes \
--subscription-arn arn:aws:sns:us-east-1:123456789012:mcp-probe-results:uuid \
--query 'Attributes.DeliveryPolicy'
Delivery status logging to CloudWatch Logs
SNS can log per-delivery outcomes (success and failure) to CloudWatch Logs. This is the only way to see why a delivery failed — without logging, SNS delivery failures are invisible (the Publish API call succeeds; failures are silent). Enable logging per-protocol on the topic (not per-subscription).
// IAM role for SNS to write delivery logs
const snsLoggingRole = new iam.Role(this, "SnsLoggingRole", {
assumedBy: new iam.ServicePrincipal("sns.amazonaws.com"),
inlinePolicies: {
Logging: new iam.PolicyDocument({
statements: [new iam.PolicyStatement({
actions: [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogGroups",
"logs:DescribeLogStreams",
],
resources: ["*"],
})],
}),
},
});
// Attach logging role to topic for SQS protocol deliveries
const cfnTopic = topic.node.defaultChild as sns.CfnTopic;
cfnTopic.sqsSuccessFeedbackRoleArn = snsLoggingRole.roleArn;
cfnTopic.sqsFailureFeedbackRoleArn = snsLoggingRole.roleArn;
cfnTopic.sqsSuccessFeedbackSampleRate = "5"; // log 5% of successes; 100% of failures always logged
Delivery logs appear in the CloudWatch log group /aws/sns/us-east-1/123456789012/DirectPublishToPhoneNumber (for SMS) or the platform-specific group. For SQS subscribers, logs are in /aws/sns/{region}/{account-id}/{topic-name}. Each log event contains status, destination, providerResponse, dwellTimeMs, and deliveryId.
Monitoring DLQ depth with CloudWatch
# Alert when SNS delivery DLQ is non-empty (any message = delivery infrastructure problem)
aws cloudwatch put-metric-alarm \
--alarm-name "sns-delivery-dlq-non-empty" \
--namespace AWS/SQS \
--metric-name ApproximateNumberOfMessagesVisible \
--dimensions Name=QueueName,Value=sns-delivery-failures \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1 \
--period 60 \
--statistic Sum \
--alarm-actions arn:aws:sns:us-east-1:123456789012:ops-alerts \
--treat-missing-data notBreaching
For AliveMCP monitoring pipelines, an SNS delivery DLQ alarm is a tier-1 alert: if probe results are not reaching the alerting consumer, AliveMCP itself has a blind spot and may not be generating alerts for endpoints that are down. Treat a non-empty SNS DLQ with the same urgency as a monitoring system outage.
DLQ message structure
When SNS writes to the subscription DLQ after exhausting retries, the SQS message contains the original SNS message body plus a special deadLetterReason field in the message attributes:
{
"Body": "<original SNS message JSON or raw body>",
"MessageAttributes": {
"deadLetterReason": {
"DataType": "String",
"StringValue": "NO_ENDPOINT_REACHABLE"
},
"RetryAttempts": {
"DataType": "Number",
"StringValue": "8"
},
"UndeliverableTimestamp": {
"DataType": "String",
"StringValue": "2026-10-29T12:34:56.789Z"
}
}
}
Common deadLetterReason values and their root causes:
| deadLetterReason | Root cause |
|---|---|
NO_ENDPOINT_REACHABLE | SQS queue does not exist, wrong ARN, or queue deleted after subscription created |
ACCESS_DENIED | SQS queue resource policy does not allow sqs:SendMessage from SNS topic |
LAMBDA_ACCESS_DENIED | Lambda resource-based policy does not allow lambda:InvokeFunction from SNS |
LAMBDA_RUNTIME_EXCEPTION | Lambda threw an unhandled exception on every delivery attempt |
KMS_ACCESS_DENIED | SQS queue's CMK does not grant SNS permission to encrypt messages |
Common failure modes
| Symptom | Cause | Fix |
|---|---|---|
DLQ receives messages but deadLetterReason is missing | Message written to SQS DLQ directly (consumer failure) rather than SNS subscription DLQ | Check which DLQ the message arrived in — SQS DLQ = consumer bug; SNS subscription DLQ = delivery infrastructure bug |
| SNS does not write to DLQ despite delivery failures | RedrivePolicy not set on subscription, or DLQ resource policy is wrong (SNS is denied write) | Verify subscription RedrivePolicy attribute; check DLQ resource policy aws:SourceArn condition |
| Delivery status logs not appearing in CloudWatch | Logging role not attached to topic, or role lacks logs:PutLogEvents on the right log group | Check SqsSuccessFeedbackRoleArn and SqsFailureFeedbackRoleArn on the topic; verify IAM role trust policy allows SNS |
| Post-backoff retries run for days before DLQ | Default retry policy has numMaxDelayRetries: 100000 — retries for 23 days | Set a custom delivery policy with numMaxDelayRetries: 0 and low numRetries total |
| Messages arrive in DLQ after consumer starts processing them successfully | Lambda function concurrency throttling during high-traffic burst caused some deliveries to exhaust retries before concurrency recovered | Increase Lambda reserved concurrency; configure provisioned concurrency; or add a buffer SQS queue between SNS and Lambda to absorb bursts |