AWS SNS: Mobile Push, Email and SMS Notification Delivery (2026)
Amazon Simple Notification Service (SNS) is AWS's fully managed pub/sub messaging service — but calling it just a pub/sub broker undersells what it actually does in production. SNS is the notification backbone for millions of applications: it dispatches transactional SMS to customers in 200+ countries, sends mobile push alerts via Apple's APNS and Google's FCM, delivers email confirmations, triggers Lambda functions, and fans out events to dozens of SQS queues simultaneously. All of this happens at AWS scale with sub-second delivery, no servers to manage, and a pay-per-message pricing model that beats any self-hosted alternative.
This guide goes deep on every SNS delivery channel — from registering device tokens for mobile push to configuring FIFO topics with deduplication, setting up DLQs for failed deliveries, and archiving messages to S3 via Kinesis Firehose. Real Python boto3 code and AWS CLI commands are included throughout.
Table of Contents
- SNS Subscription Protocols Overview
- Mobile Push: FCM (Android) and APNS (iOS)
- Email Notifications and SES vs SNS
- SMS Deep-Dive: Transactional vs Promotional
- Message Attributes and Subscription Filtering
- FIFO SNS Topics
- Delivery Status Logging and S3 Archiving
- Dead Letter Queues and Cost Optimization
SNS Subscription Protocols Overview
Every SNS topic can have multiple subscriptions, each with its own protocol. When you publish a message to the topic, SNS fans it out to every confirmed subscription simultaneously. Understanding the delivery semantics and confirmation requirements of each protocol is essential before building production workflows.
SNS supports eight subscription protocols:
- HTTP / HTTPS — SNS POSTs a JSON envelope to a public endpoint. The endpoint must respond 200 OK within 15 seconds. AWS retries with exponential backoff (20 retries over ~23 hours) on non-2xx responses.
- Email — SNS sends a plaintext or JSON email. Requires manual confirmation click from the subscriber before messages are delivered. Useful for internal alerts and notifications.
- Email-JSON — Same as Email but the body is the full SNS JSON envelope including message attributes, topic ARN, and timestamp.
- SMS — SNS delivers messages as SMS to a phone number. No subscription confirmation needed — you send directly. Subject to carrier restrictions and country-level opt-out laws.
- SQS — SNS enqueues a JSON-wrapped message into an SQS queue. The most reliable protocol because SQS provides durability. Use for decoupled async processing.
- Lambda — SNS invokes a Lambda function synchronously with the message payload. SNS retries twice on Lambda errors. For critical pipelines, use SNS → SQS → Lambda instead.
- Firehose — SNS delivers to an Amazon Kinesis Data Firehose stream, enabling fan-out to S3, Redshift, or OpenSearch for archiving and analytics.
- Application (Mobile Push) — SNS sends a push notification via a Platform Application (FCM, APNS, ADM, Baidu). The actual protocol to the device is managed by the platform — SNS handles the authentication and delivery handshake.
# Create an SNS topic
aws sns create-topic --name my-notifications
# Subscribe an SQS queue to the topic
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:123456789:my-notifications \
--protocol sqs \
--notification-endpoint arn:aws:sqs:us-east-1:123456789:my-queue
# Subscribe an HTTPS endpoint
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:123456789:my-notifications \
--protocol https \
--notification-endpoint https://api.example.com/webhooks/sns
# List all subscriptions for a topic
aws sns list-subscriptions-by-topic \
--topic-arn arn:aws:sns:us-east-1:123456789:my-notifications
SubscriptionConfirmation request type and make a GET to SubscribeURL automatically.
Each protocol has its own retry policy, message size limits, and delivery guarantees. HTTP endpoints get 3 retries in the first 20 seconds, then exponential backoff; SQS gets effectively unlimited retries as long as the queue exists; Lambda gets 2 retries then drops the message (unless you configure a DLQ). Always match the protocol to your reliability requirements.
Mobile Push: FCM (Android) and APNS (iOS)
Mobile push is where SNS shines. Instead of maintaining connections to Apple's APNs servers and Google's FCM servers yourself — managing authentication tokens, handling retries, scaling connection pools — SNS handles all of that. You register a Platform Application once, register device endpoints, and publish. SNS translates your message into the platform-native format and delivers it.
Step 1: Create a Platform Application
A Platform Application represents your app on a specific push platform. For Android/FCM, you provide your FCM Server Key (v1 API uses a service account JSON). For iOS/APNS, you provide your Apple Push Certificate or a token-based key (.p8 file).
import boto3
sns = boto3.client('sns', region_name='us-east-1')
# Create Platform Application for FCM (Android)
fcm_response = sns.create_platform_application(
Name='MyApp-Android',
Platform='GCM', # GCM = Google Cloud Messaging / FCM
Attributes={
'PlatformCredential': 'YOUR_FCM_SERVER_KEY',
'PlatformPrincipal': '', # Not needed for GCM
}
)
fcm_app_arn = fcm_response['PlatformApplicationArn']
print(f"FCM Platform Application ARN: {fcm_app_arn}")
# Create Platform Application for APNS (iOS) — token-based auth
apns_response = sns.create_platform_application(
Name='MyApp-iOS',
Platform='APNS', # Use APNS_SANDBOX for development
Attributes={
'PlatformCredential': open('AuthKey_XXXXXXXXXX.p8').read(),
'PlatformPrincipal': 'YOUR_10_CHAR_KEY_ID',
'ApplePlatformTeamID': 'YOUR_TEAM_ID',
'ApplePlatformBundleID': 'com.yourcompany.yourapp',
}
)
apns_app_arn = apns_response['PlatformApplicationArn']
print(f"APNS Platform Application ARN: {apns_app_arn}")
Step 2: Register Device Endpoints
When a user installs your app and grants notification permission, the OS provides a device token (FCM registration token or APNs device token). Your backend calls create_platform_endpoint to register it with SNS. Store the resulting endpoint ARN in your user database — this is what you'll publish to for unicast (single-user) notifications.
def register_device(platform_app_arn: str, device_token: str, user_id: str) -> str:
"""Register or update a device endpoint. Returns endpoint ARN."""
try:
response = sns.create_platform_endpoint(
PlatformApplicationArn=platform_app_arn,
Token=device_token,
CustomUserData=user_id, # Store user_id for your own reference
Attributes={
'Enabled': 'true',
}
)
endpoint_arn = response['EndpointArn']
print(f"Created endpoint: {endpoint_arn}")
return endpoint_arn
except sns.exceptions.InvalidParameterException as e:
# Endpoint already exists — extract ARN from error message
import re
match = re.search(r'Endpoint (arn:aws:sns[^ ]+) already exists', str(e))
if match:
endpoint_arn = match.group(1)
# Update the token in case it refreshed
sns.set_endpoint_attributes(
EndpointArn=endpoint_arn,
Attributes={
'Token': device_token,
'Enabled': 'true',
}
)
return endpoint_arn
raise
Step 3: Publish a Push Notification
SNS uses a structured JSON message format when publishing to mobile endpoints. The MessageStructure='json' parameter tells SNS to route the GCM or APNS key to the appropriate platform. This lets you craft platform-specific payloads in a single publish call.
import json
def send_push_notification(endpoint_arn: str, title: str, body: str, data: dict = None):
"""Send a push notification to a single device."""
message = {
'GCM': json.dumps({
'notification': {
'title': title,
'body': body,
'sound': 'default',
},
'data': data or {},
'priority': 'high',
}),
'APNS': json.dumps({
'aps': {
'alert': {
'title': title,
'body': body,
},
'sound': 'default',
'badge': 1,
},
**(data or {}),
}),
'APNS_SANDBOX': json.dumps({
'aps': {
'alert': {'title': title, 'body': body},
'sound': 'default',
},
}),
'default': f"{title}: {body}",
}
try:
response = sns.publish(
TargetArn=endpoint_arn,
Message=json.dumps(message),
MessageStructure='json',
MessageAttributes={
'notificationType': {
'DataType': 'String',
'StringValue': 'transactional',
}
}
)
return response['MessageId']
except sns.exceptions.EndpointDisabledException:
# Device token is stale — remove from your DB
print(f"Endpoint disabled: {endpoint_arn}. Remove from database.")
return None
EndpointDisabledException by disabling or deleting the endpoint ARN and updating your user record. For iOS, APNS tokens rarely change but can after a device restore.
For broadcast push (send to all users), subscribe all endpoint ARNs to a topic and publish once to the topic. For audience segments (e.g., "all iOS users in the US"), use SNS message filtering with subscription filter policies on attributes like platform and region.
Email Notifications and SES vs SNS Email
SNS email subscriptions are quick to set up and useful for internal alerts — infrastructure notifications, deployment events, error summaries. However, they come with important limitations that determine when you should use SES instead.
SNS Email Subscription Flow
When you create an email subscription, SNS sends a confirmation email with a SubscribeURL link. The recipient must click this link before SNS will deliver any messages to that address. This double opt-in is by design — it prevents you from spamming arbitrary addresses via SNS.
import boto3
sns = boto3.client('sns', region_name='us-east-1')
# Subscribe an email address to an SNS topic
response = sns.subscribe(
TopicArn='arn:aws:sns:us-east-1:123456789:alerts-topic',
Protocol='email',
Endpoint='ops-team@yourcompany.com',
ReturnSubscriptionArn=False # ARN only available after confirmation
)
print("Confirmation email sent. Subscription pending until confirmed.")
# For JSON-formatted email (includes full SNS envelope)
sns.subscribe(
TopicArn='arn:aws:sns:us-east-1:123456789:alerts-topic',
Protocol='email-json',
Endpoint='devops@yourcompany.com'
)
SNS email has a critical limitation: you cannot customize the From address, subject line formatting beyond the topic's DisplayName, or HTML email body. The email body is the raw message string. For customer-facing email with branding, HTML templates, and deliverability management, use Amazon SES.
When to Use SNS Email vs SES
| Feature | SNS Email | Amazon SES |
|---|---|---|
| Setup complexity | Minimal | Requires domain verification, DKIM |
| HTML email support | No (plaintext only) | Yes (full HTML + attachments) |
| Custom From address | No | Yes (any verified domain) |
| Deliverability management | Basic | Full (bounce, complaint handling) |
| Bulk sending | Not designed for it | Yes (up to 50,000/day sandbox, unlimited production) |
| Best for | Internal ops alerts, monitoring | Transactional & marketing email |
To publish a message that arrives cleanly in email, set the SNS topic's DisplayName attribute — this becomes the sender name in the email client. Keep messages concise since there's no HTML rendering, and include the relevant context (resource ID, metric value, timestamp) directly in the message body.
# Set a display name so emails show "Techoral Alerts" instead of a raw ARN
aws sns set-topic-attributes \
--topic-arn arn:aws:sns:us-east-1:123456789:alerts-topic \
--attribute-name DisplayName \
--attribute-value "Techoral Alerts"
# Publish a message (arrives as email to all confirmed email subscribers)
aws sns publish \
--topic-arn arn:aws:sns:us-east-1:123456789:alerts-topic \
--subject "Deploy Failed: api-service v2.3.1" \
--message "Deployment of api-service v2.3.1 failed at 14:23 UTC.
Region: us-east-1
Error: Health check timeout after 5 minutes
Action required: Check ECS service events and CloudWatch logs."
SMS Deep-Dive: Transactional vs Promotional
SNS SMS delivery is powerful but nuanced. Before sending a single SMS in production, understand the two message types, the account-level spending limit, opt-out handling, and country-level restrictions — getting these wrong leads to blocked messages, unexpected bills, or regulatory violations.
Transactional vs Promotional SMS
Every SNS SMS message is classified as either Transactional or Promotional. This classification affects routing, deliverability, and in some countries (like India with the DND registry) whether the message is delivered at all.
- Transactional — OTPs, authentication codes, order confirmations, bank alerts. Highest priority routing. Delivered even to DND-registered numbers in India. More expensive.
- Promotional — marketing offers, announcements, newsletters. Standard routing. Blocked by DND registrations. Cheaper.
import boto3
sns = boto3.client('sns', region_name='us-east-1')
def send_sms(phone_number: str, message: str, message_type: str = 'Transactional',
sender_id: str = None) -> dict:
"""
Send an SMS via SNS.
phone_number: E.164 format, e.g. '+12025551234' or '+919876543210'
message_type: 'Transactional' or 'Promotional'
sender_id: Alphanumeric sender ID (not supported in US/Canada)
"""
attributes = {
'AWS.SNS.SMS.SMSType': {
'DataType': 'String',
'StringValue': message_type,
},
}
if sender_id:
attributes['AWS.SNS.SMS.SenderID'] = {
'DataType': 'String',
'StringValue': sender_id,
}
response = sns.publish(
PhoneNumber=phone_number,
Message=message,
MessageAttributes=attributes,
)
return {'MessageId': response['MessageId'], 'Status': 'sent'}
# Send an OTP (Transactional)
send_sms('+919876543210', 'Your OTP is 847291. Valid for 5 minutes.', 'Transactional')
# Send a promotion (Promotional)
send_sms('+919876543210', 'Get 20% off this weekend! Use code SAVE20.', 'Promotional')
Bulk SMS Sender with Rate Control
import time
import boto3
from typing import List
sns = boto3.client('sns', region_name='us-east-1')
def bulk_sms(recipients: List[dict], message_template: str,
message_type: str = 'Transactional', rate_per_second: int = 20):
"""
Send SMS to a list of recipients with rate limiting.
recipients: [{'phone': '+1...', 'name': 'Alice', 'data': {...}}]
"""
results = {'sent': 0, 'failed': 0, 'errors': []}
interval = 1.0 / rate_per_second
for i, recipient in enumerate(recipients):
phone = recipient['phone']
# Personalize message (simple template replacement)
message = message_template.format(**recipient.get('data', {}),
name=recipient.get('name', ''))
try:
sns.publish(
PhoneNumber=phone,
Message=message[:160], # SMS segment limit
MessageAttributes={
'AWS.SNS.SMS.SMSType': {'DataType': 'String', 'StringValue': message_type}
}
)
results['sent'] += 1
except Exception as e:
results['failed'] += 1
results['errors'].append({'phone': phone, 'error': str(e)})
# Rate limiting — SNS default is 20 TPS per account for SMS
if (i + 1) % rate_per_second == 0:
time.sleep(1)
else:
time.sleep(interval)
return results
SMSMonthToDateSpentUSD to prevent surprise bills.
Opt-Out Handling
In the US, recipients can text STOP to opt out. SNS manages opt-out lists automatically — opted-out numbers are added to the account-level opt-out list and will not receive further messages. You can check and manage this list programmatically:
# List opted-out phone numbers
aws sns list-phone-numbers-opted-out
# Check if a specific number has opted out
aws sns check-if-phone-number-is-opted-out \
--phone-number +12025551234
# Re-opt-in a number (only if the user explicitly requested it)
aws sns opt-in-phone-number \
--phone-number +12025551234
Message Attributes and Subscription Filtering
Without filtering, every SNS subscriber receives every message published to the topic. That's inefficient when different subscribers care about different event types. SNS subscription filter policies let each subscriber define exactly which messages it wants, based on message attributes. This eliminates the need for separate topics per event type.
Publishing with Message Attributes
import boto3, json
sns = boto3.client('sns', region_name='us-east-1')
# Publish an order event with attributes for filtering
sns.publish(
TopicArn='arn:aws:sns:us-east-1:123456789:order-events',
Message=json.dumps({
'orderId': 'ORD-9841',
'customerId': 'CUST-4521',
'total': 149.99,
'currency': 'USD',
'status': 'placed',
}),
MessageAttributes={
'eventType': {
'DataType': 'String',
'StringValue': 'order.placed',
},
'region': {
'DataType': 'String',
'StringValue': 'us-east-1',
},
'totalAmount': {
'DataType': 'Number',
'StringValue': '149.99',
},
'isPrime': {
'DataType': 'String',
'StringValue': 'true',
}
}
)
Configuring Subscription Filter Policies
# Filter policy: only receive order.placed events with totalAmount >= 100 from Prime customers
filter_policy = {
"eventType": ["order.placed", "order.confirmed"],
"totalAmount": [{"numeric": [">=", 100]}],
"isPrime": ["true"]
}
# Apply filter policy when subscribing (or update existing subscription)
sns.subscribe(
TopicArn='arn:aws:sns:us-east-1:123456789:order-events',
Protocol='sqs',
Endpoint='arn:aws:sqs:us-east-1:123456789:high-value-orders',
Attributes={
'FilterPolicy': json.dumps(filter_policy),
'FilterPolicyScope': 'MessageAttributes', # or 'MessageBody'
}
)
# Another subscriber receives ALL order events (no filter)
sns.subscribe(
TopicArn='arn:aws:sns:us-east-1:123456789:order-events',
Protocol='sqs',
Endpoint='arn:aws:sqs:us-east-1:123456789:all-orders-audit',
)
MessageAttributes (default) to filter on message attributes, or MessageBody to filter on fields inside the JSON message body. MessageBody filtering requires the message to be valid JSON. Combine both for complex routing — e.g., route by eventType attribute and then check a nested status field inside the body.
Platform-Specific Message Overrides (Mobile Push)
When publishing to a topic with both FCM and APNS subscribers, you can provide platform-specific payloads in a single publish call. SNS routes the correct payload to each platform:
message_payload = {
"default": "You have a new message",
"GCM": json.dumps({
"notification": {"title": "New Message", "body": "Alice sent you a photo"},
"data": {"chatId": "CHT-882", "type": "photo"},
"android": {"priority": "high", "ttl": "3600s"}
}),
"APNS": json.dumps({
"aps": {
"alert": {"title": "New Message", "body": "Alice sent you a photo"},
"badge": 3,
"sound": "chime.wav",
"mutable-content": 1, # Allows notification service extension
},
"chatId": "CHT-882",
"type": "photo",
}),
"APNS_SANDBOX": json.dumps({
"aps": {"alert": {"title": "New Message", "body": "Alice sent you a photo"}}
})
}
sns.publish(
TopicArn='arn:aws:sns:us-east-1:123456789:push-broadcast',
Message=json.dumps(message_payload),
MessageStructure='json',
)
FIFO SNS Topics
Standard SNS topics are eventually consistent and deliver messages without strict ordering guarantees — adequate for most notification use cases where "at-least-once, approximate order" is fine. FIFO SNS topics (available since 2020) add exactly-once delivery, strict message ordering per Message Group ID, and deduplication — at the cost of throughput (300 publishes/second, 3,000 with batching).
When to Use FIFO Topics
Use FIFO SNS when downstream consumers must process events in the order they occurred. Classic examples: financial transaction ledgers, inventory state machines, and order lifecycle events (placed → confirmed → shipped → delivered) where processing "shipped" before "placed" would corrupt state.
# Create a FIFO SNS topic (name must end in .fifo)
aws sns create-topic \
--name order-lifecycle.fifo \
--attributes '{
"FifoTopic": "true",
"ContentBasedDeduplication": "false"
}'
# FIFO SNS can only subscribe to FIFO SQS queues
# Create a FIFO SQS queue
aws sqs create-queue \
--queue-name order-processing.fifo \
--attributes '{
"FifoQueue": "true",
"ContentBasedDeduplication": "false"
}'
# Subscribe FIFO SQS to FIFO SNS
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:123456789:order-lifecycle.fifo \
--protocol sqs \
--notification-endpoint arn:aws:sqs:us-east-1:123456789:order-processing.fifo
import boto3, json, hashlib
sns = boto3.client('sns', region_name='us-east-1')
def publish_order_event(order_id: str, event_type: str, payload: dict):
"""Publish an order event with guaranteed ordering and deduplication."""
# Deduplication ID: hash of order_id + event_type + timestamp bucket
# Using content-based dedup would auto-hash the message body
dedup_id = hashlib.md5(f"{order_id}:{event_type}".encode()).hexdigest()
response = sns.publish(
TopicArn='arn:aws:sns:us-east-1:123456789:order-lifecycle.fifo',
Message=json.dumps(payload),
Subject=f"Order {event_type}",
MessageGroupId=order_id, # All events for same order are ordered
MessageDeduplicationId=dedup_id, # Prevents duplicates within 5-min window
MessageAttributes={
'eventType': {'DataType': 'String', 'StringValue': event_type}
}
)
return response['MessageId']
# Events for the same order will be delivered in publish order
publish_order_event('ORD-9841', 'order.placed', {'total': 149.99, 'items': 3})
publish_order_event('ORD-9841', 'order.confirmed', {'warehouse': 'BLR-1'})
publish_order_event('ORD-9841', 'order.shipped', {'trackingId': 'DTDC123'})
MessageGroupId controls the ordering scope. All messages with the same MessageGroupId are delivered in order, but messages across different groups can be delivered in parallel — allowing horizontal scaling while maintaining per-entity ordering. Use order_id, user_id, or account_id as the group ID depending on your consistency requirement.
Delivery Status Logging and S3 Archiving
SNS delivery status logging gives you visibility into whether each message was successfully delivered to each subscriber. Without it, you're flying blind — a silent delivery failure to an HTTP endpoint or a disabled mobile endpoint goes unnoticed. Enable delivery status logging to CloudWatch Logs for every production topic.
Enabling Delivery Status Logging
import boto3
sns = boto3.client('sns', region_name='us-east-1')
iam = boto3.client('iam', region_name='us-east-1')
# First, create an IAM role that allows SNS to write to CloudWatch Logs
# (In production, use CloudFormation or CDK for this)
topic_arn = 'arn:aws:sns:us-east-1:123456789:my-notifications'
# Enable delivery status logging for HTTP/HTTPS (100% sample rate)
sns.set_topic_attributes(
TopicArn=topic_arn,
AttributeName='HTTPSuccessFeedbackRoleArn',
AttributeValue='arn:aws:iam::123456789:role/SNSDeliveryFeedback'
)
sns.set_topic_attributes(
TopicArn=topic_arn,
AttributeName='HTTPFailureFeedbackRoleArn',
AttributeValue='arn:aws:iam::123456789:role/SNSDeliveryFeedback'
)
sns.set_topic_attributes(
TopicArn=topic_arn,
AttributeName='HTTPSuccessFeedbackSampleRate',
AttributeValue='100' # Log 100% of successful deliveries
)
# Enable for mobile push (GCM/FCM)
sns.set_topic_attributes(
TopicArn=topic_arn,
AttributeName='ApplicationSuccessFeedbackRoleArn',
AttributeValue='arn:aws:iam::123456789:role/SNSDeliveryFeedback'
)
sns.set_topic_attributes(
TopicArn=topic_arn,
AttributeName='ApplicationFailureFeedbackRoleArn',
AttributeValue='arn:aws:iam::123456789:role/SNSDeliveryFeedback'
)
Delivery status logs appear in CloudWatch Logs under /aws/sns/deliverylogs/<protocol>. Each log entry includes the message ID, subscriber endpoint ARN, delivery status, status code, dwell time (time from publish to delivery attempt), and total time.
SNS to S3 via Kinesis Firehose
For long-term message archiving, analytics, or compliance, subscribe a Kinesis Data Firehose delivery stream to your SNS topic. Firehose buffers and batches records before writing to S3, with configurable prefix, format (JSON lines, Parquet), and compression.
# Create Firehose delivery stream to S3 (abbreviated — use CDK/CloudFormation in production)
aws firehose create-delivery-stream \
--delivery-stream-name sns-archive \
--delivery-stream-type DirectPut \
--s3-destination-configuration '{
"RoleARN": "arn:aws:iam::123456789:role/FirehoseS3Role",
"BucketARN": "arn:aws:s3:::my-sns-archive-bucket",
"Prefix": "sns/year=!{timestamp:yyyy}/month=!{timestamp:MM}/day=!{timestamp:dd}/",
"ErrorOutputPrefix": "sns-errors/",
"BufferingHints": {"SizeInMBs": 5, "IntervalInSeconds": 300},
"CompressionFormat": "GZIP"
}'
# Subscribe Firehose to SNS topic
aws sns subscribe \
--topic-arn arn:aws:sns:us-east-1:123456789:my-notifications \
--protocol firehose \
--notification-endpoint arn:aws:firehose:us-east-1:123456789:deliverystream/sns-archive \
--attributes '{"SubscriptionRoleArn": "arn:aws:iam::123456789:role/SNSFirehoseRole"}'
Dead Letter Queues and Cost Optimization
Even with SNS's robust retry logic, some messages fail permanent delivery. HTTP endpoints can go offline, Lambda functions can error past the retry limit, mobile endpoints can be permanently disabled. Without a Dead Letter Queue (DLQ), those messages are silently dropped. A DLQ captures failed deliveries so you can inspect, replay, or alert on them.
Configuring SQS DLQ for SNS Subscriptions
Each SNS subscription can have its own DLQ — a dedicated SQS queue where SNS sends messages that failed all delivery retries. This is separate from the SQS queue DLQ (which handles messages that consumers failed to process).
import boto3, json
sns = boto3.client('sns', region_name='us-east-1')
sqs = boto3.client('sqs', region_name='us-east-1')
# Create the DLQ
dlq_response = sqs.create_queue(
QueueName='sns-notifications-dlq',
Attributes={'MessageRetentionPeriod': '1209600'} # 14 days
)
dlq_url = dlq_response['QueueUrl']
dlq_arn = sqs.get_queue_attributes(
QueueUrl=dlq_url, AttributeNames=['QueueArn']
)['Attributes']['QueueArn']
# Allow SNS to send to the DLQ (SQS resource policy)
dlq_policy = {
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "sns.amazonaws.com"},
"Action": "sqs:SendMessage",
"Resource": dlq_arn,
"Condition": {
"ArnEquals": {
"aws:SourceArn": "arn:aws:sns:us-east-1:123456789:my-notifications"
}
}
}]
}
sqs.set_queue_attributes(
QueueUrl=dlq_url,
Attributes={'Policy': json.dumps(dlq_policy)}
)
# Subscribe an HTTP endpoint with a DLQ
response = sns.subscribe(
TopicArn='arn:aws:sns:us-east-1:123456789:my-notifications',
Protocol='https',
Endpoint='https://api.example.com/webhooks/sns',
Attributes={
'RedrivePolicy': json.dumps({
'deadLetterTargetArn': dlq_arn
})
}
)
print(f"Subscription with DLQ: {response['SubscriptionArn']}")
Monitoring Undelivered Messages
# CloudWatch alarm on DLQ depth
aws cloudwatch put-metric-alarm \
--alarm-name "SNS-DLQ-Messages" \
--metric-name "ApproximateNumberOfMessagesVisible" \
--namespace "AWS/SQS" \
--dimensions Name=QueueName,Value=sns-notifications-dlq \
--statistic Sum \
--period 60 \
--threshold 1 \
--comparison-operator GreaterThanOrEqualToThreshold \
--evaluation-periods 1 \
--alarm-actions arn:aws:sns:us-east-1:123456789:ops-alerts \
--treat-missing-data notBreaching
start-message-move-task API. Set a manageable replay rate to avoid overwhelming the downstream subscriber.
Cost Optimization
SNS costs are low but add up at scale. Here's a breakdown of the main cost drivers:
| Channel | Cost | Notes |
|---|---|---|
| API requests (publish) | $0.50 per million | Each publish = 1 request regardless of subscribers |
| HTTP/SQS/Lambda deliveries | $0.50 per million | Each delivery to each subscriber counts |
| Mobile push (free tier) | 1 million free/month | First 1M push deliveries free per month |
| SMS — US | $0.00645 per message | Transactional |
| SMS — India | ~$0.0023 per message | Varies by carrier |
| SMS — UK | ~$0.0424 per message | Higher in Western Europe |
| $2 per 100,000 | Only SNS charges — no SES costs for SNS-sent email | |
| Firehose deliveries | $0.19 per GB | Firehose itself has separate charges |
Key cost optimization strategies:
- Use subscription filtering to avoid delivering irrelevant messages. Without filtering, you pay per delivery to every subscriber even if most ignore the message.
- Batch SMS with message compression — a single 160-character SMS counts as one message; messages over 160 chars are split into segments and billed per segment. Keep transactional SMS under 160 chars.
- Fan-out to SQS, not Lambda directly — SNS to Lambda invocations at scale are billed per Lambda invocation. Routing through SQS allows batching (one Lambda invocation processes 10 messages) reducing Lambda costs by 10x.
- Disable delivery status logging for low-risk topics — 100% sample rate logging generates significant CloudWatch Logs ingestion costs. Use 5-10% sample rate for high-volume topics where you only need statistical visibility.
- Clean up disabled mobile endpoints — disabled endpoints still count in your endpoint quota and incur minimal management overhead. Use SNS's
list-endpoints-by-platform-applicationto find and delete endpoints withEnabled=false.
import boto3
sns = boto3.client('sns', region_name='us-east-1')
def cleanup_disabled_endpoints(platform_app_arn: str) -> int:
"""Remove disabled (stale) endpoints from a Platform Application."""
paginator = sns.get_paginator('list_endpoints_by_platform_application')
deleted = 0
for page in paginator.paginate(PlatformApplicationArn=platform_app_arn):
for endpoint in page['Endpoints']:
if endpoint['Attributes'].get('Enabled') == 'false':
sns.delete_endpoint(EndpointArn=endpoint['EndpointArn'])
deleted += 1
print(f"Deleted {deleted} disabled endpoints from {platform_app_arn}")
return deleted