SNS
SNS is a custom Python reimplementation. 36 of the 42 SNS operations are implemented in LocalEmu's own code: standard and FIFO topics, nine subscription protocols (HTTP, HTTPS, email, email-json, SMS, SQS, application, Lambda, Firehose), filter policies on message attributes or message body, per-subscription DLQs, FIFO content-based deduplication with a 5-minute window, subscription confirmation tokens, application platform endpoints for mobile push, phone-number opt-out, and three CloudFormation resource types.
SMS and mobile push (the sms and application subscription protocols) do not reach a real carrier or push service. Messages are stored in LocalEmu and can be inspected over HTTP at /_aws/sns/sms-messages and /_aws/sns/platform-endpoint-messages. The other seven protocols deliver to the corresponding LocalEmu service.
Operation-level coverage: see the SNS coverage matrix.
Quick start
Create a topic, subscribe an SQS queue, publish a message, read it from the queue.
$ awsemu sns create-topic --name orders
{
"TopicArn": "arn:aws:sns:us-east-1:000000000000:orders"
}
$ awsemu sqs create-queue --queue-name order-events
$ QURL=http://sqs.us-east-1.localhost:4566/000000000000/order-events
$ QARN=$(awsemu sqs get-queue-attributes --queue-url $QURL \
--attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
$ awsemu sns subscribe \
--topic-arn arn:aws:sns:us-east-1:000000000000:orders \
--protocol sqs \
--notification-endpoint $QARN
$ awsemu sns publish \
--topic-arn arn:aws:sns:us-east-1:000000000000:orders \
--message '{"order_id":"o-42","total":99}'
$ awsemu sqs receive-message --queue-url $QURL --query 'Messages[0].Body' --output text
{"Type":"Notification","MessageId":"...","TopicArn":"arn:aws:sns:us-east-1:000000000000:orders","Message":"{\"order_id\":\"o-42\",\"total\":99}", ...} Architecture
SNS lives at services/sns/. The provider class SnsProvider (provider.py:156) handles the API surface and routes published messages through the PublishDispatcher:
- •Publish dispatch at
publisher.py:1248: looks up every subscription on the topic, applies each subscription's filter policy, and forwards the message to the protocol-specific publisher. - •Per-topic ordering at
publisher.py:1332-1335: FIFO topics use a partitioned thread-pool executor keyed byMessageGroupId, so messages within the same group are processed serially; standard topics use a shared executor. - •Filter policy evaluator at
filter.py: parses and applies filter policies against either the message attributes (default) or the message body. Up to 5 keys per policy, up to 150 value combinations. - •FIFO dedup cache at
models.py:218-219: per-topic map of dedup-id to (message-id, sequence-number, timestamp). Entries older than the 5-minute window are pruned on each publish. Mutation is protected by a threading lock. - •Confirmation tokens at
provider.py:469-492: 288-hex-character tokens minted onSubscribefor HTTP/HTTPS subscribers and posted to the subscriber endpoint inside aSubscriptionConfirmationenvelope. Non-HTTP subscribers (SQS, Lambda, Firehose, application, SMS) are auto-confirmed.
State lives in SnsStore (models.py:182): topics, subscriptions, FIFO dedup cache, platform applications, platform endpoint messages, SMS messages, opted-out phone numbers, and subscription confirmation tokens. When PERSISTENCE=1 is set, the store is serialised and restored across restarts.
Subscription protocols
Nine protocols are accepted by Subscribe (services/sns/constants.py:7-17). Each has its own publisher class in services/sns/publisher.py.
| Protocol | Delivery | Source |
|---|---|---|
sqs | SendMessage (single) or SendMessageBatch to the subscribed queue. | publisher.py:314,416 |
lambda | Invoke with InvocationType=Event (async). | publisher.py:193 |
http / https | POST to the subscriber endpoint with the SNS notification envelope. Requires confirmation via the SubscribeURL in the envelope. | publisher.py:517 |
email / email-json | SES SendEmail from SNS_SES_SENDER_ADDRESS (falls back to no-reply@localemu.cloud). The subscription stays in PendingConfirmation until ConfirmSubscription is called. | publisher.py:602,596 |
firehose | Firehose PutRecord to the configured delivery stream. SubscriptionRoleArn is required. | publisher.py:769 |
sms | Stored only. The message is appended to an in-memory buffer (max 10 000 messages). No carrier delivery. Inspect via GET /_aws/sns/sms-messages. | publisher.py:704 |
application | Stored only. Messages for mobile push platform endpoints (APNs, FCM, ADM, etc.) are stored, not forwarded to a real push service. Inspect via GET /_aws/sns/platform-endpoint-messages. | publisher.py:647 |
Configuration
| Variable | Default | Purpose |
|---|---|---|
SNS_SES_SENDER_ADDRESS | no-reply@localemu.cloud | From address used when SNS sends an email subscription notification via SES. Set this to a verified SES identity if you turn on SES sender enforcement. |
SNS_CERT_URL_HOST | (unset) | Override the host portion of the certificate URL embedded in HTTP/HTTPS notification envelopes. Subscribers that verify SNS message signatures fetch the certificate from this host. |
Features supported
| Feature | Notes |
|---|---|
| Topic lifecycle | CreateTopic, ListTopics, GetTopicAttributes, SetTopicAttributes, DeleteTopic. |
| Standard and FIFO topics | FIFO topics end in .fifo and require FifoTopic=true. A FIFO topic can only subscribe to FIFO queues; a standard topic can subscribe to either. |
| Publish | Publish and PublishBatch. FIFO topics return SequenceNumber on each publish. |
| Subscribe and confirm | Subscribe, ConfirmSubscription, Unsubscribe, ListSubscriptions, ListSubscriptionsByTopic, GetSubscriptionAttributes, SetSubscriptionAttributes. |
| Subscription attributes | FilterPolicy, FilterPolicyScope, RawMessageDelivery, RedrivePolicy, DeliveryPolicy, SubscriptionRoleArn. |
| Filter policies | Scope = MessageAttributes (default) or MessageBody. Operators: string, prefix, suffix, numeric (<, <=, =, >=, >), anything-but, exists. Max 5 keys, max 150 value combinations. |
| Per-subscription DLQ | RedrivePolicy points at an SQS queue. Failed deliveries land there with the original message. |
| Raw message delivery | RawMessageDelivery=true sends the bare message body to the subscriber instead of the SNS envelope. |
| FIFO content-based deduplication | ContentBasedDeduplication=true derives the dedup ID from SHA-256 of the body. Otherwise MessageDeduplicationId is required on publish. |
| Topic tags | TagResource, UntagResource, ListTagsForResource. |
| Topic policy | AddPermission, RemovePermission. Policy is stored and returned but not enforced on incoming requests. |
| Mobile push platform endpoints | CreatePlatformApplication, CreatePlatformEndpoint, GetEndpointAttributes, SetEndpointAttributes, DeleteEndpoint. Push delivery is stored only. |
| Phone-number opt-out | CheckIfPhoneNumberIsOptedOut, OptInPhoneNumber, ListPhoneNumbersOptedOut. |
| CloudFormation | AWS::SNS::Topic, AWS::SNS::Subscription, AWS::SNS::TopicPolicy. |
FIFO topics
FIFO topics preserve publish order within a MessageGroupId and deduplicate messages within a 5-minute window. The dispatch executor partitions work by group ID, so messages in the same group run serially while different groups run in parallel.
- •Required on publish:
MessageGroupId. EitherMessageDeduplicationIdmust be set, or the topic must haveContentBasedDeduplication=true. - •Dedup window: 5 minutes. Re-publishing the same dedup ID inside the window returns the original
MessageIdand silently drops the duplicate. - •Sequence numbers: the response includes
SequenceNumber, monotonic across all FIFO topics in the account. - •Subscriber constraint: a FIFO topic can only subscribe a FIFO SQS queue. The validation runs at
provider.py:396-400.
$ awsemu sns create-topic --name orders.fifo \
--attributes FifoTopic=true,ContentBasedDeduplication=true
$ awsemu sqs create-queue --queue-name order-events.fifo \
--attributes FifoQueue=true,ContentBasedDeduplication=true
$ QARN=arn:aws:sqs:us-east-1:000000000000:order-events.fifo
$ awsemu sns subscribe \
--topic-arn arn:aws:sns:us-east-1:000000000000:orders.fifo \
--protocol sqs \
--notification-endpoint $QARN
# Three messages, same MessageGroupId => strict order downstream
$ for i in 1 2 3; do
awsemu sns publish \
--topic-arn arn:aws:sns:us-east-1:000000000000:orders.fifo \
--message-group-id customer-42 \
--message "{\"order\": $i}"
done
$ awsemu sqs receive-message \
--queue-url http://sqs.us-east-1.localhost:4566/000000000000/order-events.fifo \
--max-number-of-messages 10 --wait-time-seconds 2 \
--query 'Messages[].Body' --output json | python3 -c "import sys,json; [print(json.loads(m)[\"Message\"]) for m in json.load(sys.stdin)]"
{"order": 1}
{"order": 2}
{"order": 3} Filter policies
A subscription's FilterPolicy attribute decides which published messages reach that subscription. The policy is JSON; each key maps to a list of value matchers. The default FilterPolicyScope is MessageAttributes; set it to MessageBody to match against the message body instead.
Operators are exact-string, numeric ranges, prefix, suffix, anything-but, and exists. A policy can declare up to 5 keys and the cross-product of all value alternatives must not exceed 150 combinations (filter.py:290 and filter.py:298).
$ awsemu sns subscribe \
--topic-arn arn:aws:sns:us-east-1:000000000000:orders \
--protocol sqs \
--notification-endpoint $QARN \
--attributes '{
"FilterPolicy": "{\"priority\":[\"high\"],\"total\":[{\"numeric\":[\">\",100]}]}",
"FilterPolicyScope": "MessageAttributes"
}'
# This message matches: priority=high AND total>100. It is delivered.
$ awsemu sns publish \
--topic-arn arn:aws:sns:us-east-1:000000000000:orders \
--message 'priority order' \
--message-attributes '{
"priority": {"DataType":"String","StringValue":"high"},
"total": {"DataType":"Number","StringValue":"500"}
}'
# This message does NOT match (priority=low). It is dropped before reaching the queue.
$ awsemu sns publish \
--topic-arn arn:aws:sns:us-east-1:000000000000:orders \
--message 'noise' \
--message-attributes '{
"priority": {"DataType":"String","StringValue":"low"}
}' Wire a topic to Lambda
$ awsemu lambda create-function \
--function-name on-order \
--runtime python3.12 \
--role arn:aws:iam::000000000000:role/lambda-role \
--handler handler.handler \
--zip-file fileb://handler.zip
$ FARN=arn:aws:lambda:us-east-1:000000000000:function:on-order
$ awsemu sns subscribe \
--topic-arn arn:aws:sns:us-east-1:000000000000:orders \
--protocol lambda \
--notification-endpoint $FARN
$ awsemu sns publish \
--topic-arn arn:aws:sns:us-east-1:000000000000:orders \
--message '{"order_id":"o-99"}'
# The Lambda is invoked asynchronously with the SNS event envelope
$ awsemu logs tail /aws/lambda/on-order --follow Inspecting stored protocols
SMS and mobile push do not leave LocalEmu. The published messages are kept in memory and four internal HTTP endpoints let you inspect them:
| Endpoint | Contents |
|---|---|
GET /_aws/sns/sms-messages | Every SMS that would have been sent through the sms protocol or a direct phone-number publish. Bounded to the last 10 000 messages. |
GET /_aws/sns/platform-endpoint-messages | Every message routed to a mobile-push platform endpoint, grouped by endpoint ARN. |
GET /_aws/sns/subscription-tokens | Map of confirmation tokens to subscription ARNs. Use the token to call ConfirmSubscription for HTTP/HTTPS or email subscriptions. |
GET /_aws/sns/phone-opt-outs | Phone numbers that called OptOutPhoneNumber. Direct publishes and sms protocol subscriptions to these numbers are dropped. |
# Inspect the SMS publish history (sms protocol + direct phone-number publishes)
$ curl -s http://localhost:4566/_aws/sns/sms-messages
# Inspect mobile push history (application platform endpoints)
$ curl -s http://localhost:4566/_aws/sns/platform-endpoint-messages
# Look up subscription confirmation tokens
$ curl -s http://localhost:4566/_aws/sns/subscription-tokens
# Phone numbers that called OptOutPhoneNumber
$ curl -s http://localhost:4566/_aws/sns/phone-opt-outs Subscription confirmation
When you call Subscribe, the behaviour depends on the protocol:
| Protocol | Confirmation behaviour |
|---|---|
sqs, lambda, firehose, application, sms | Auto-confirmed at Subscribe time. PendingConfirmation is immediately false. |
http, https | SNS POSTs a SubscriptionConfirmation envelope to the subscriber URL. The subscriber must call the SubscribeURL inside that envelope (a GET to ConfirmSubscription with a token) before deliveries start. |
email, email-json | The subscription stays in PendingConfirmation. The confirmation email is sent through SES. Look up the token at /_aws/sns/subscription-tokens and call ConfirmSubscription manually. |
Tokens are 288 hex characters and encode the region. Token validation strips region back out so cross-region confirmations work without extra plumbing.
Limits and defaults
| Limit | LocalEmu | AWS | Source |
|---|---|---|---|
| Max message size | 256 KiB | 256 KiB | services/sns/constants.py:64 |
| FIFO dedup window | 5 min | 5 min | services/sns/provider.py:836 |
| Filter policy keys | 5 | 5 | services/sns/filter.py:290 |
| Filter policy combinations | 150 | 150 | services/sns/filter.py:298 |
ListTopics / ListSubscriptions page size | 100 | 100 | services/sns/provider.py:331,691,711 |
| SMS message buffer | 10 000 (in-memory) | n/a | services/sns/models.py:211 |
| Max subject length | not enforced | 100 chars | LocalEmu accepts longer subjects. |
| Max message attributes per publish | not enforced | 10 | LocalEmu accepts more. |
Known limitations
- •SMS does not reach a carrier. Messages published through the
smsprotocol or a direct phone-number publish are stored at/_aws/sns/sms-messages. No real SMS is sent. - •Mobile push does not reach APNs, FCM, or any other push service. Messages routed to platform endpoints are stored at
/_aws/sns/platform-endpoint-messages. - •Email confirmation links are not delivered for you. Email subscriptions stay in
PendingConfirmation. Either fetch the token from/_aws/sns/subscription-tokensand callConfirmSubscription, or run an SES inbox-watcher to follow the link. - •Server-side encryption (
KmsMasterKeyId) is not implemented. Setting the attribute has no effect on the stored message bodies. - •Topic policy is stored, not enforced.
AddPermissionandRemovePermissionupdate thePolicyattribute, but cross-account publishes and subscribes are accepted regardless of the policy contents. - •Subscription-level tags are stored only partially.
TagResourceon a subscription ARN succeeds, but the tags do not round-trip throughGetSubscriptionAttributes. - •SMS Sandbox is not implemented. Six operations return
NotImplementedException:CreateSMSSandboxPhoneNumber,DeleteSMSSandboxPhoneNumber,GetSMSSandboxAccountStatus,ListOriginationNumbers,ListSMSSandboxPhoneNumbers,VerifySMSSandboxPhoneNumber.