AWS Deep Dives - Amazon SNS (Simple Notification Service)
Amazon SNS (Simple Notification Service) is a fully managed publish-subscribe (pub/sub) messaging service that enables fan-out communication between applications and directly to end users.
When you start exploring the long list of AWS services, you realize that many things are simple. I don’t mean that they are necessarily easy. What I mean is that they literally have the word “simple” in their name, as does every service we’ve covered in this series thus far (SES, SQS, and now SNS).
Amazon SNS stands for “Simple Notification Service,” and it is a publish-subscribe (aka pub/sub) notification service. On the one hand, like SQS, which we covered previously, it helps you to follow a well-architected serverless application architecture that follows the single responsibility principle with services that are loosely coupled. But, on the other hand, SNS supports endpoints like HTTP(S), email, mobile push notifications, text messages, and more that allow notifications to be broadcast outside your serverless application. Let’s take a deeper look.
Why Do You Need a Push Notification Service?
The tricky part about SNS isn’t so much understanding why you need it, but trying to grasp all the potential use cases. That’s because the pub/sub model is incredibly flexible and serves both internal (i.e. application-to-application) and external (i.e., application-to-person) use cases. Let’s look at some key ones.
- Event Fan-Out Communication Between Internal Services: This is where SNS helps support a decoupled architecture, similar to SQS. However, whereas messages on SQS queues are typically consumed once by a single service, SNS allows a service to publish an event once, and multiple internal or even external systems (via HTTP endpoints) will react to that event independently (i.e., why it’s called “fan-out”). For example, a single purchase event on an e-commerce system might trigger an inventory update, a billing notification, and a shipping workflow. In fact, SNS and SQS often work together, with SQS queues subscribing to an SNS topic while also ensuring things like retries and buffering for each individual workflow triggered by the SNS event.
- External Notifications to Users or Systems: SNS has the ability to reach out to external systems (via HTTP endpoints such as APIs or webhooks) and external users (via email, mobile push notifications, or text messaging). For example, imagine our shipping workflow actually utilizes an external, 3rd party provider; the same SNS event could both trigger the HTTP endpoint to start the shipping process while also notifying the end-user via text message that their shipment is being prepared. Retry policies and dead-letter queues can help ensure critical notifications aren’t missed.
- Operational Alerts: Because of SNS’s ability to communicate with both internal AWS services as well as external services, it can be ideal for operational alerts that need to reach both people and tools (ex. PagerDuty, Slack) at the same time. When also combined with the ability to filter message subscriptions, it can ensure that each endpoint only receives relevant messages.
- Event Routing: While not as robust as EventBridge, SNS’s fan-out and filtering capabilities make it an option as a lightweight event router, where consumers self-select relevant events via filtering.
That’s a lot of places where SNS may fit into your application, and this is far from an exhaustive list of use cases.
Example Repository
The example code shown here is from a sample application available on GitHub that aims to demonstrate a number of SNS use cases in a simple e-commerce order management backend. The example application can be deployed to LocalStack or AWS using the AWS CDK and demonstrates SNS message topics, subscriptions, application-to-application messaging, and application-to-person messaging.
SNS Basics
There are two core aspects to a pub/sub service like SNS, and, yeah, they are probably pretty obvious: publishing messages to a topic and subscribing to a topic. But, within those basics, there are a lot of options. I can’t cover everything here, but I’ll try to cover some of the key items. As with the prior articles in this series, the example code will use the AWS CDK.
Creating an SNS Topic
When it comes to creating a topic, SNS lives up to the “simple” in its name. There are literally no required properties when constructing a topic via CDK/CloudFormation. Technically, a topic name is required; if you don’t provide one, CloudFormation generates a unique physical ID for the name.
const orderEventsTopic = new sns.Topic(this, 'OrderEventsTopic', { topicName: 'order-events', displayName: 'Events from e-commerce orders',});If you need to ensure that SNS maintains a strict message order, you’ll want to create a FIFO (first in, first out) topic. This ensures that the messages are delivered in the order they are received. Just as with SQS, the name of a FIFO topic must end in .fifo, and SNS also provides deduplication options if needed.
const myFifoTopic = new sns.Topic(this, 'OrderEventsTopic', { topicName: 'order-events.fifo', // Name MUST end in .fifo fifo: true,});It’s worth noting that if you combine FIFO deduplication with filtering, which we’ll discuss in more detail later, SNS cannot guarantee exactly-once message delivery because it’s possible that an individual message is filtered out and never received.
Publishing to Topics
There are two steps to publishing to a topic. First, we need to grant the publisher permission to publish to the topic. In the example below, we’re using CDK to create a Lambda, providing that Lambda with the SNS topic ARN as an environment variable and, finally, granting the Lambda permission to publish to the appropriate topic.
// 1. Order API Lambda (Publisher)const orderApiLambda = new NodejsFunction(this, 'OrderApiLambda', { ...lambdaDefaults, functionName: 'order-api', entry: 'lambda/order-api.ts', handler: 'handler', environment: { SNS_TOPIC_ARN: orderEventsTopic.topicArn, },});
// Grant the Order API Lambda permission to publish to the topicorderEventsTopic.grantPublish(orderApiLambda);At this point, our Lambda has permission to publish, but nothing has been published yet. Let’s look at how to publish to the SNS topic from within the Lambda. To do this, we’ll use PublishCommand, which is exported from the AWS SDK’s SNS client (@aws-sdk/client-sns).
We’ll provide the topic ARN that we supplied as an environment variable via the CDK above. The message field will provide the JSON payload.
Message attributes are arbitrary structured metadata that we can add to our message. As we’ll see in further examples, these are useful for providing details that can be used to decide how to handle a message without processing the message body. For example, we are providing an eventType which, in the case of our sample app, can be OrderPlaced, OrderShipped, OrderDelayed, and OrderCancelled that we’ll use to determine how to route a message within the business logic of a Lambda subscribed to this topic.
Finally, we’ll use the SDK’s SNS client to send the message to the topic, which will trigger any resources subscribed to that topic.
// Publish to SNS with message attributes for filteringconst publishCommand = new PublishCommand({ TopicArn: SNS_TOPIC_ARN, Message: JSON.stringify(messagePayload), MessageAttributes: { eventType: { DataType: 'String', StringValue: orderEvent.eventType, }, priority: { DataType: 'String', StringValue: orderEvent.priority || 'medium', }, },});
const result = await snsClient.send(publishCommand);So let’s see how we would subscribe to and respond to messages published to the topic.
Subscriptions
Adding a subscription can be relatively simple as well. For instance, the CDK example below from our sample app adds a topic subscription to a Lambda. The most complicated aspect of this is the filter policy, but it isn’t a required element. If we didn’t provide one, the Lambda would, as you might have guessed, get all the messages from that topic.
orderEventsTopic.addSubscription( new snsSubscriptions.LambdaSubscription(customerNotificationsLambda, { filterPolicy: { eventType: sns.SubscriptionFilter.stringFilter({ allowlist: ['OrderPlaced', 'OrderShipped', 'OrderCancelled'], }), }, }),);Filtering
As you can see from above, filtering allows us to constrain the messages that will be passed to a resource. In the case of this example, we are using one of the arbitrary message attributes that we created earlier and simply filtering messages with the eventType of OrderDelayed (as that is the only supported eventType that isn’t listed in the allowList).
This example is very simple, but filtering can get much more complex if needed, and a full overview of writing filter policies is beyond the scope of this blog post (you can reference the filtering section of the SNS documentation or this blog post), but let’s briefly touch on a couple of important notes:
- You can filter on both the message attributes (i.e., the arbitrary metadata) or the message body by setting the
FilterPolicyScope. We have not set the scope above because message attributes are the default scope. Using message attributes for filtering is considered best practice when possible, but there are circumstances where you cannot modify the SNS message, such as native AWS Service events (e.g., S3, CloudWatch, CloudFront), for example, where payload filtering may be necessary. Performance is cited as one reason to use message attributes since SNS doesn’t need to parse the message body (though I can’t seem to easily find data that clearly backs this up), but the more important reason is that message body filtering is not free, costing $0.09 per GB of scanned payload data if using US-East-1, for instance. - Filter policies can include a decent amount of logic by utilizing the supported filter operators. For instance, a policy can include AND/OR logic. The
allowListshown above is essentially OR logic, wherein any of the values foreventTypewill pass through. This could potentially have been achieved via an anything-but filter, where we would have allowed any value exceptOrderDelayed. You can do exact matches, matches on prefixes or suffixes, IP address matching, numeric value range matching, and more. The point here is that the logic in your filter policy can probably get as complex as you need it to.
Handling an SNS event in a Lambda
Let’s briefly look at how you might handle an SNS message event within code in a Lambda. In the example below, the Lambda is receiving the messages of all the allowed eventTypes from the prior example. We’ll parse the message body of the event within our handler and then call different methods within the Lambda function based upon the eventType message attribute, passing the parsed message body data along to the function.
export const handler = async (event: SNSEvent): Promise<void> => { console.log( 'Customer Notifications Lambda received event:', JSON.stringify(event, null, 2), );
for (const record of event.Records) { const message: OrderEvent = JSON.parse(record.Sns.Message);
console.log('Processing customer notification:', { eventType: message.eventType, orderId: message.orderId, userId: message.userId, });
// Send SMS notifications based on event type switch (message.eventType) { case 'OrderPlaced': await sendOrderConfirmation(message); break; case 'OrderShipped': await sendShippingNotification(message); break; case 'OrderCancelled': await sendCancellationNotification(message); break; default: console.log( `Unhandled event type for customer notifications: ${message.eventType}`, ); } }};Sending messages to Lambda is one example of application-to-application messaging within SNS. Other types of application-to-application subscribers include SQS and HTTP endpoints.
Application to Person Messaging
In addition to application-to-application messaging, SNS is capable of application-to-person messaging via SMS text messages, mobile push notifications, or email. SNS is not as full-featured for application to person messaging as something like AWS End User Messaging or SES. SNS is most useful for quick and easy system/application alerts or notifications that fan out to many subscribers (note: ultimately, SMS messages sent through SNS are delivered via AWS End User Messaging SMS).
As you might expect, there are also verification steps required for sending messages to individuals. For SMS text messages, SNS provides a sandbox with a limited number of manually verified numbers, but you’ll eventually need to go through a verification process to move out of the sandbox, which could take some time (a minimum of 24 hours). Email subscribers will need to confirm their subscription to the topic.
Sending SMS Text Messages
Before we can send text messages within our Lambda, we need to grant permission for our Lambda to send SMS messages. In our case, we are sending messages to individual phone numbers for notifications rather than to a list of numbers subscribed to a topic. This cannot use the typical grantPublish as we aren’t just pushing messages onto the topic. We need to enable sns:Publish on * because phone numbers are not ARNs and can’t be scoped in the policy.
// Grant permission to send SMS via SNScustomerNotificationsLambda.addToRolePolicy( new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['sns:Publish'], resources: ['*'], // Required for SMS publishing to phone numbers }),);From within the Lambda itself, sending the message isn’t complicated. We’ll just use the PublishCommand from the AWS SDK’s SNS client.
async function sendSMS(phoneNumber: string, message: string): Promise<void> { if (!phoneNumber) { console.warn('No phone number provided, skipping SMS'); return; }
try { const command = new PublishCommand({ PhoneNumber: phoneNumber, Message: message, });
const result = await snsClient.send(command); console.log(`SMS sent successfully. MessageId: ${result.MessageId}`); } catch (error) { console.error(`Failed to send SMS to ${phoneNumber}:`, error); throw error; }}It’s critical to keep in mind that there are a significant number of laws that regulate text messaging individuals, so it is worth reviewing the best practices guide before proceeding.
Sending Emails
We’ve previously discussed SES as a full-featured service for sending emails that is typically used for transactional and/or marketing emails. However, if your application needs to send a simple, text-only email notification, such as a system alert or some other form of internal notification, SNS can get the job done simply (and less expensively than SES).
In our example, we’re manually adding an email subscription to the topic via the CDK since the email is for an ops alert when an issue occurs.
// Create SNS Topic for Ops Alerts (email notifications)
const opsAlertsTopic = new sns.Topic(this, 'OpsAlertsTopic', { topicName: 'ops-alerts', displayName: 'Operations Team Alerts',});
// Add email subscription for ops teamopsAlertsTopic.addSubscription( new snsSubscriptions.EmailSubscription('ops@foo.com'),);From within the Lambda, we need to publish to the topic, and all the subscribed emails (which is only one email address in our simple example) will receive the update.
const command = new PublishCommand({ TopicArn: OPS_ALERTS_TOPIC_ARN, Subject: emailSubject, Message: emailBody,});
const result = await snsClient.send(command);It’s worth pointing out that the email body cannot be personalized. Everyone subscribed gets the same message, which reemphasizes that this is more useful for alerts than marketing emails.
SNS Costs
So what’s this all going to cost you? As usual, when speaking about AWS services, the answer is, “It depends on a lot of factors.” Let’s dig into the pricing based on using US-EAST-1 as the baseline.
You typically pay for SNS based upon API requests (publishes, subscription operations, etc.). The good news is that, for standard topic requests, the first million SNS requests per month are free, and after that, it’s $0.50 per additional 1 million requests. A caveat to this is that each 64KB chunk of published data is billed as one request and, since a request can have a payload up to 256KB, that would be billed as four requests.
The pricing can also be different based on how you are delivering the messages. You can deliver to services like Lambda and SQS for no additional charge (I mean, other than what you are paying for those services downstream) but delivering to mobile push notifications (1 million free and $0.50 per million thereafter), email (1,000 free and $2.00 per 100,000 thereafter) and HTTP/S (100,000 free and $0.60 per million thereafter) will cost you extra. SMS (text message) pricing is tougher to predict because it depends on the destination country and any carrier fees.
FIFO topics will also cost you extra, and do not have a free tier. The cost is $0.30 per million plus $0.017 per GB of payload data. In addition, there are no free deliveries on FIFO topics, which cost $0.01 per 1 million and $0.001 per GB of payload data.
Additional features like message filtering, archiving/replay, and data protection can also introduce additional charges.
LocalStack Support
The good news is that SNS has excellent coverage in LocalStack, and that coverage is available within our free account. Almost all the AWS APIs for SNS are covered, with the only exceptions being the APIs around managing SMS sandboxes.
If you are working with application-to-person messaging, you can fully test this on LocalStack, but be aware that the messages will not be sent. However, you can use LocalStack’s provided developer endpoints to confirm messages have been sent. Or, in the case of email, you can also use the Mailhog extension to view sent emails.
Additional Resources
I hope you found this introductory deep dive helpful. If you are looking to learn more about SNS, here are the official documentation resources and other blog posts that you may find useful.
- What is Amazon SNS? (AWS documentation)
- Simple Notification Service (SNS) (LocalStack docs)
- AWS SNS: The Complete Guide to Real-Time Notifications (DataCamp blog)
- AWS SNS vs. SQS - What Are the Main Differences? (AWS Fundamentals blog)