← Back to DevBytes

Scaling SNS: From Prototype to Production

Introduction to Scaling SNS

Amazon Simple Notification Service (SNS) is a fully managed pub/sub messaging service that enables you to decouple microservices, distribute notifications, and fan-out messages to multiple subscribers. While building a prototype with SNS is straightforward—create a topic, subscribe an endpoint, publish a message—scaling that prototype to production introduces a host of challenges around throughput, reliability, cost, and observability.

This tutorial walks through the journey of taking an SNS-based architecture from a quick prototype to a robust, production-grade system. We will cover the fundamentals, identify common scaling bottlenecks, and provide practical code examples and best practices along the way.

What Is Amazon SNS?

Amazon SNS is a push-based messaging platform that supports two primary patterns: application-to-application (A2A) pub/sub and application-to-person (A2P) notifications. In the A2A model, a publisher sends messages to a topic, and SNS fans those messages out to subscribed endpoints such as AWS Lambda, SQS queues, HTTP/S webhooks, email, or mobile push services.

Key characteristics of SNS include:

Why Scaling SNS Matters

In a prototype, you might publish a few hundred messages per minute to a single topic with one or two subscribers. Everything works fine. But as your application grows, several issues emerge:

Scaling SNS is less about SNS itself—AWS handles the infrastructure—and more about how you design the surrounding system: subscription patterns, message payloads, error handling, and observability.

From Prototype to Production: The Architecture

The Prototype

A typical prototype looks like this: a single SNS topic receives events from an application, and a Lambda function is subscribed directly to process them. Here is a minimal example using the AWS SDK for Python (Boto3):

import boto3

sns = boto3.client('sns', region_name='us-east-1')

# Create a topic
topic = sns.create_topic(Name='orders-topic')
topic_arn = topic['TopicArn']

# Subscribe a Lambda function
sns.subscribe(
    TopicArn=topic_arn,
    Protocol='lambda',
    Endpoint='arn:aws:lambda:us-east-1:123456789012:function:process-order'
)

# Publish a message
sns.publish(
    TopicArn=topic_arn,
    Message='{"orderId": "12345", "status": "created"}'
)

This works for a demo, but it has problems: no error handling, no dead-letter queue, no batching, and no visibility into failures. Let's evolve it.

Production Architecture

In production, the recommended pattern is to use SNS as a fan-out router with SQS queues as buffers between SNS and your consumers. This decouples message production from consumption, absorbs traffic spikes, and enables retry logic per consumer.

import boto3

sns = boto3.client('sns')
sqs = boto3.client('sqs')

# 1. Create the SNS topic
topic = sns.create_topic(Name='orders-prod')
topic_arn = topic['TopicArn']

# 2. Create an SQS queue with a dead-letter queue
dlq = sqs.create_queue(QueueName='orders-dlq')
dlq_arn = sqs.get_queue_attributes(
    QueueUrl=dlq['QueueUrl'],
    AttributeNames=['QueueArn']
)['Attributes']['QueueArn']

main_queue = sqs.create_queue(
    QueueName='orders-queue',
    Attributes={
        'RedrivePolicy': f'{{"deadLetterTargetArn":"{dlq_arn}","maxReceiveCount":"5"}}',
        'VisibilityTimeout': '60'
    }
)
main_queue_arn = sqs.get_queue_attributes(
    QueueUrl=main_queue['QueueUrl'],
    AttributeNames=['QueueArn']
)['Attributes']['QueueArn']

# 3. Allow SNS to send messages to the SQS queue
sqs.set_queue_attributes(
    QueueUrl=main_queue['QueueUrl'],
    Attributes={
        'Policy': json.dumps({
            "Version": "2012-10-17",
            "Statement": [{
                "Effect": "Allow",
                "Principal": {"Service": "sns.amazonaws.com"},
                "Action": "sqs:SendMessage",
                "Resource": main_queue_arn,
                "Condition": {
                    "ArnEquals": {"aws:SourceArn": topic_arn}
                }
            }]
        })
    }
)

# 4. Subscribe the SQS queue to the SNS topic
sns.subscribe(
    TopicArn=topic_arn,
    Protocol='sqs',
    Endpoint=main_queue_arn
)

With this setup, SNS fans out messages to SQS, which acts as a durable buffer. Your consumer (Lambda, ECS, or EC2 worker) pulls from SQS at its own pace. Failed messages after five retries move to the DLQ for manual inspection.

Optimizing Message Publishing

Batch Publishing

For high-throughput workloads, use the publish_batch API to send up to 10 messages in a single request. This reduces API calls, lowers cost, and improves throughput.

import boto3

sns = boto3.client('sns')
topic_arn = 'arn:aws:sns:us-east-1:123456789012:orders-prod'

messages = [
    {"id": f"msg-{i}", "orderId": str(i), "status": "created"}
    for i in range(10)
]

response = sns.publish_batch(
    TopicArn=topic_arn,
    PublishBatchRequestEntries=[
        {
            'Id': msg['id'],
            'Message': json.dumps(msg),
            'MessageAttributes': {
                'eventType': {
                    'DataType': 'String',
                    'StringValue': 'order.created'
                }
            }
        }
        for msg in messages
    ]
)

print(f"Success: {len(response.get('Successful', []))}")
print(f"Failed: {len(response.get('Failed', []))}")

Message Filtering

When multiple subscribers are attached to one topic, use message attribute filtering so each subscriber only receives relevant messages. This reduces unnecessary processing and cost.

# Subscribe with a filter policy
sns.subscribe(
    TopicArn=topic_arn,
    Protocol='sqs',
    Endpoint=queue_arn,
    Attributes={
        'FilterPolicy': json.dumps({
            "eventType": ["order.created", "order.updated"]
        })
    },
    ReturnSubscriptionArn=True
)

With this filter, the subscriber only receives messages where the eventType attribute matches order.created or order.updated. Messages with other event types are never delivered to this queue, saving downstream compute.

Handling Payload Size Limits

SNS has a 256 KB payload limit per message. In production, you should avoid sending large payloads through SNS entirely. Instead, use the claim-check pattern: store the full payload in S3 and send a reference through SNS.

import boto3
import json
import uuid

s3 = boto3.client('s3')
sns = boto3.client('sns')

def publish_large_event(topic_arn, event_data, bucket_name):
    # Store the full payload in S3
    object_key = f"events/{uuid.uuid4()}.json"
    s3.put_object(
        Bucket=bucket_name,
        Key=object_key,
        Body=json.dumps(event_data).encode('utf-8')
    )

    # Publish a lightweight reference message
    sns.publish(
        TopicArn=topic_arn,
        Message=json.dumps({
            'bucket': bucket_name,
            'key': object_key,
            'eventType': event_data.get('eventType')
        }),
        MessageAttributes={
            'eventType': {
                'DataType': 'String',
                'StringValue': event_data.get('eventType', 'unknown')
            }
        }
    )

The consumer retrieves the full payload from S3 when processing. This keeps SNS fast and cheap while supporting arbitrarily large payloads.

Observability and Monitoring

At scale, you need visibility into publish rates, delivery failures, and subscription health. Enable SNS topic metrics and set up CloudWatch alarms for critical signals.

import boto3

cloudwatch = boto3.client('cloudwatch')

# Alarm for delivery failures
cloudwatch.put_metric_alarm(
    AlarmName='sns-orders-delivery-failures',
    AlarmDescription='Alert when SNS delivery failures exceed threshold',
    MetricName='NumberOfNotificationsFailed',
    Namespace='AWS/SNS',
    Statistic='Sum',
    Period=300,
    EvaluationPeriods=1,
    Threshold=10,
    ComparisonOperator='GreaterThanThreshold',
    Dimensions=[
        {'Name': 'TopicName', 'Value': 'orders-prod'}
    ],
    AlarmActions=[
        'arn:aws:sns:us-east-1:123456789012:ops-alerts'
    ]
)

Key metrics to monitor include NumberOfMessagesPublished, NumberOfNotificationsDelivered, NumberOfNotificationsFailed, and PublishSize. Also enable delivery status logging for HTTP/S subscriptions to capture detailed failure reasons.

Best Practices for Production SNS

Securing SNS at Scale

Security becomes critical as your topic count and subscriber base grow. Start by enabling KMS encryption on topics containing sensitive data:

sns = boto3.client('sns')

sns.create_topic(
    Name='sensitive-events',
    Attributes={
        'KmsMasterKeyId': 'arn:aws:kms:us-east-1:123456789012:key/your-key-id'
    }
)

Apply a topic access policy that restricts publishing to specific IAM roles and limits cross-account subscriptions:

policy = {
    "Version": "2012-10-17",
    "Id": "restricted-topic-policy",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::123456789012:role/producer-role"
            },
            "Action": "sns:Publish",
            "Resource": topic_arn
        },
        {
            "Effect": "Allow",
            "Principal": {"Service": "sqs.amazonaws.com"},
            "Action": "sns:Subscribe",
            "Resource": topic_arn,
            "Condition": {
                "StringEquals": {
                    "aws:SourceAccount": "123456789012"
                }
            }
        }
    ]
}

sns.set_topic_attributes(
    TopicArn=topic_arn,
    AttributeName='Policy',
    AttributeValue=json.dumps(policy)
)

Conclusion

Scaling Amazon SNS from a prototype to a production system is fundamentally about designing the ecosystem around the service rather than the service itself. By introducing SQS buffers, implementing the claim-check pattern for large payloads, applying message filtering, enabling dead-letter queues, and establishing robust observability, you transform a simple pub/sub prototype into a resilient, cost-effective, and observable event-driven architecture. The key is to treat SNS as a high-throughput router—not a storage layer—and to push complexity like retries, ordering, and payload management into the surrounding components where you have fine-grained control. With these patterns in place, your SNS-based system can handle millions of events per day while remaining maintainable and secure.

— Ad —

Google AdSense will appear here after approval

← Back to all articles