This website uses cookies

Read our Privacy policy and Terms of use for more information.

What it actually is

Amazon SNS is a publish/subscribe router with no storage you can read from. A publisher sends one message to a topic. SNS looks at every subscription on that topic, decides which ones should get it, and pushes a copy to each endpoint: an SQS queue, a Lambda function, an HTTP endpoint, an email address, a phone number, a mobile push token, or an Amazon Data Firehose stream.

The word that matters is push. SQS holds messages until you come and ask for them. SNS holds nothing for you to ask about. It makes delivery attempts on your behalf, and when it runs out of attempts the message is gone, unless you attached a dead-letter queue to that specific subscription. Most of the production incidents involving SNS are variations on that one sentence.

SNS is also not one service. There are standard topics, FIFO topics, application-to-person delivery (SMS, email, mobile push), and a growing list of features bolted onto the topic: payload-based filtering, message data protection, archiving and replay. This article is about the application-to-application part, because that is where engineers make architecture decisions.

The model

The primitives are few. A topic is a named channel with an ARN. A subscription binds one endpoint to one topic and carries its own attributes: a filter policy, a raw-delivery flag, a redrive policy (the dead-letter queue), and for HTTP endpoints a delivery policy. A message is a body plus up to 10 attributes, and the publisher can send up to 10 messages in one PublishBatch call.

Size is the first contract. The default maximum message size is 256 KiB. In September 2026 AWS added a MaximumMessageSize topic attribute that raises the limit to 1 MiB on both standard and FIFO topics. There is a catch that is easy to miss: once a topic is set above 256 KiB, it can have at most 100 subscriptions, and every one of them must be SQS, Lambda, or Firehose. SNS rejects the change with InvalidParameter if the topic has an HTTP, email, SMS, or push subscriber. Beyond 1 MiB you use the Extended Client Library, which parks the payload in S3 and sends a pointer, up to 2 GB.

Delivery is the second contract. For AWS-managed endpoints (SQS, Lambda, Firehose) the retry policy is fixed and generous: 3 immediate attempts, 2 more one second apart, 10 exponential attempts from 1 to 20 seconds, then 100,000 attempts 20 seconds apart. That is 100,015 attempts over 23 days. For customer-managed endpoints (email, SMS, mobile push) it is 50 attempts over 6 hours. For HTTP/S you define the policy yourself, with a hard ceiling of 3,600 seconds of total retry time. SNS retries only server-side failures: 5xx and 429 for HTTP. A 4xx is a permanent failure and the message is dropped immediately.

Ordering and duplicates are the third contract. A standard topic is at-least-once with best-effort ordering. A FIFO topic orders within a message group and deduplicates on a 5-minute window, but its subscribers are limited to SQS queues. FIFO topics sustain 300 messages per second per message group and, by default, 3,000 messages per second or 20 MB per second per topic. Setting FifoThroughputScope to MessageGroup lifts the per-topic cap.

What AWS operates: the topic, the fan-out, the retries, the storage during retries. What you operate: the access policies, the filter policies, the decision about what happens to a message that never lands, and the idempotency of every consumer.

When to use it, when not to

People confuse SNS with SQS and EventBridge, and the three are not interchangeable.

SNS

SQS

EventBridge

Shape

Push to many subscribers

Pull by one consumer group

Rules route events to targets

Stores messages for you

No (retries only, up to 23 days for managed endpoints)

Yes, up to 14 days

Archive and replay optional

Filtering

Filter policy per subscription, attribute or body

None

Event patterns on the full event

Fan-out target types

SQS, Lambda, HTTP, email, SMS, push, Firehose

Single queue

Many AWS services, API destinations

Pick it when

One event, several independent consumers, low latency

You need a buffer and backpressure

You route by rich content or integrate SaaS and AWS events

The honest rule of thumb: if the consumer needs a buffer, put SQS in front of it, and use SNS to feed several of those buffers. SNS plus SQS is the canonical fan-out, and it is the one we build below. If you find yourself writing a long filter policy that mirrors an event schema, EventBridge is probably the better router. If you are pushing to a phone, SNS is the only one of the three that does it.

Do not use SNS as a queue, as a log, or as the system of record for anything. It will not hold a message for a consumer that is down unless you put a queue between them.

What it costs

SNS bills per request and per delivery, and the dimensions depend on the topic type. AWS publishes the structure on its pricing page but renders the per-unit dollar rates dynamically, so treat the figures below as a snapshot from a third-party tracker dated March 2026 for US East (N. Virginia), and check the pricing page before you budget.

For standard topics, a publish is one billed request, at about $0.50 per million with the first million free each month. Delivery to SQS and Lambda has no per-message delivery charge, though data transfer is billed at internet rates for traffic that leaves the region. HTTP/S deliveries are about $0.06 per 100,000, and email is about $2.00 per 100,000. SMS varies by country and carries carrier fees.

For FIFO topics, you pay for published messages (about $0.30 per million), for subscribed messages (about $0.30 per million, and filtered and delivered both count), and for payload data (about $0.11 per GB).

The surprise is chunk billing. Every 64 KB chunk of a payload is billed as one request and, for deliveries, one delivery. A 1 MiB publish is billed as 16 requests and 16 deliveries. The 1 MiB limit is a capability, not a free upgrade. Payload-based filtering also bills: attribute filtering is free, but body filtering is charged on the outbound payload data scanned per subscription with an active policy, with a 1 KB minimum per message. Archiving bills on stored data and time, with a 1-day minimum.

Following along in this article costs well under a dollar. The few messages we publish sit inside the free tier.

The limits that bite

The soft limits that matter at scale are the publish rate and the topic counts. Publish throughput is per account per region and combines Publish and PublishBatch: 30,000 messages per second in US East (N. Virginia), 9,000 in US West (Oregon) and Europe (Ireland), 1,500 standard and 3,000 FIFO in a set of mid-size regions (including Frankfurt, Tokyo, Singapore, Sydney, Ohio), and 300 standard and 3,000 FIFO elsewhere. You get 100,000 standard topics and 1,000 FIFO topics per account. A standard topic can hold 12,500,000 subscriptions, a FIFO topic only 100.

Filter policies have their own budget: 200 per topic and 10,000 per account, 5 keys per policy, a complexity score (the product of the match-value counts) of at most 150, and 256 KB per policy. Attribute filtering does not allow nested policies; body filtering does, and only leaf keys count toward the 5-key limit. String matching is case-sensitive.

Hard limits worth knowing: Subscribe and Unsubscribe are capped at 100 TPS each, an email or email-json subscription is capped at 10 messages per second and that cannot be raised, and a filter policy change can take up to 15 minutes to fully take effect. That last one has burned more engineers than any quota. You change a policy, publish a test message, see no delivery, and conclude your policy is wrong. It might be right and simply not live yet.

Build it

We build a topic called orders, publish order events, and fan them out to three SQS queues: an audit queue that receives everything, a fulfilment queue that receives only order_created events, and a high-value queue that receives orders over 500 using a body-scoped filter. The fulfilment subscription gets a dead-letter queue. Then we add a FIFO topic with archive and replay.

Prerequisites. Python 3.10 or later, boto3 installed, and an AWS account where you can create resources in us-east-1. Credentials come from your usual profile. The IAM identity needs these actions: sns:CreateTopic, sns:DeleteTopic, sns:Subscribe, sns:Unsubscribe, sns:SetSubscriptionAttributes, sns:SetTopicAttributes, sns:GetTopicAttributes, sns:Publish, sns:ListSubscriptionsByTopic, and sqs:CreateQueue, sqs:DeleteQueue, sqs:GetQueueAttributes, sqs:SetQueueAttributes, sqs:ReceiveMessage, sqs:PurgeQueue.

Step 1: the topic and the queues. We create the topic with a 1 MiB limit to show the new attribute, then four queues (three consumers and one dead-letter queue).

import json, boto3

REGION = "us-east-1"
sns = boto3.client("sns", region_name=REGION)
sqs = boto3.client("sqs", region_name=REGION)

topic_arn = sns.create_topic(
    Name="orders",
    Attributes={"MaximumMessageSize": "1048576"},
)["TopicArn"]

def make_queue(name):
    url = sqs.create_queue(QueueName=name)["QueueUrl"]
    arn = sqs.get_queue_attributes(
        QueueUrl=url, AttributeNames=["QueueArn"]
    )["Attributes"]["QueueArn"]
    return url, arn

queues = {n: make_queue(n) for n in
          ["orders-audit", "orders-fulfilment", "orders-highvalue", "orders-fulfilment-dlq"]}

CreateTopic is idempotent for the same name and attributes, so rerunning this is safe. The MaximumMessageSize attribute is accepted on CreateTopic and SetTopicAttributes, and Publish validates the combined size of body and attributes against it.

Step 2: let SNS write to the queues. An SQS queue refuses messages from SNS until its access policy says otherwise. Scope the policy to this topic with aws:SourceArn, not to the whole service.

def allow_topic(queue_url, queue_arn):
    policy = {
        "Version": "2012-10-17",
        "Statement": [{
            "Sid": "AllowOrdersTopic",
            "Effect": "Allow",
            "Principal": {"Service": "sns.amazonaws.com"},
            "Action": "sqs:SendMessage",
            "Resource": queue_arn,
            "Condition": {"ArnEquals": {"aws:SourceArn": topic_arn}},
        }],
    }
    sqs.set_queue_attributes(
        QueueUrl=queue_url, Attributes={"Policy": json.dumps(policy)})

for name in ["orders-audit", "orders-fulfilment", "orders-highvalue"]:
    allow_topic(*queues[name])

The dead-letter queue needs the same treatment, because SNS itself writes undeliverable messages to it. We apply it in step 4.

Step 3: subscribe with filter policies. The audit queue has no filter, so it receives everything. The fulfilment queue uses the default attribute scope. The high-value queue uses the body scope with a numeric operator.

def subscribe(queue_arn, attrs=None):
    return sns.subscribe(
        TopicArn=topic_arn, Protocol="sqs", Endpoint=queue_arn,
        Attributes=attrs or {}, ReturnSubscriptionArn=True,
    )["SubscriptionArn"]

audit_sub = subscribe(queues["orders-audit"][1], {"RawMessageDelivery": "true"})

fulfil_sub = subscribe(queues["orders-fulfilment"][1], {
    "RawMessageDelivery": "true",
    "FilterPolicy": json.dumps({"event_type": ["order_created"]}),
})

high_sub = subscribe(queues["orders-highvalue"][1], {
    "RawMessageDelivery": "true",
    "FilterPolicyScope": "MessageBody",
    "FilterPolicy": json.dumps({"order": {"total": [{"numeric": [">", 500]}]}}),
})

Raw message delivery strips the SNS JSON envelope, so your consumer reads the body you published. Without it, each SQS message is an envelope with Type, MessageId, TopicArn, Message (your body as an escaped string), and Timestamp, and you have to unwrap it. Turn it on unless you need the envelope.

Attribute filtering matches the keys you set in MessageAttributes at publish time. Body filtering matches the JSON structure of the message body, so the policy mirrors the shape of the payload. Within one key, multiple values are OR. Across keys, it is AND. Remember the propagation delay: these policies can take up to 15 minutes to be fully effective.

Step 4: attach the dead-letter queue to the subscription. This is the step most tutorials skip, and it is the one that matters. The redrive policy is a subscription attribute, and the DLQ is a plain SQS queue that SNS is allowed to write to.

dlq_url, dlq_arn = queues["orders-fulfilment-dlq"]
allow_topic(dlq_url, dlq_arn)

sns.set_subscription_attributes(
    SubscriptionArn=fulfil_sub,
    AttributeName="RedrivePolicy",
    AttributeValue=json.dumps({"deadLetterTargetArn": dlq_arn}),
)

Be precise about what this DLQ catches. It receives messages SNS could not deliver to the endpoint: the queue was deleted, the access policy was removed, or the retries ran out. It does not catch messages that were delivered fine and that your consumer then failed to process. That failure belongs to the SQS queue's own redrive policy. Two different dead-letter queues guard two different failures, and you want both.

Step 5: publish with attributes. We publish four orders. The attributes carry the event type for the fulfilment filter, and the body carries the total for the high-value filter.

orders = [
    ("order_created", 120), ("order_created", 900),
    ("order_cancelled", 45), ("order_created", 15),
]
for i, (etype, total) in enumerate(orders):
    sns.publish(
        TopicArn=topic_arn,
        Message=json.dumps({"order": {"id": f"o-{i}", "total": total}}),
        MessageAttributes={
            "event_type": {"DataType": "String", "StringValue": etype}
        },
    )

Verify it works

Give new or changed filter policies up to 15 minutes to take effect before you judge the result. Then drain each queue and count.

def drain(name):
    url = queues[name][0]
    got = []
    while True:
        r = sqs.receive_message(QueueUrl=url, MaxNumberOfMessages=10, WaitTimeSeconds=5)
        msgs = r.get("Messages", [])
        if not msgs:
            return got
        got += [json.loads(m["Body"]) for m in msgs]
        for m in msgs:
            sqs.delete_message(QueueUrl=url, ReceiptHandle=m["ReceiptHandle"])

for n in ["orders-audit", "orders-fulfilment", "orders-highvalue"]:
    print(n, len(drain(n)))

The expected counts from the four messages above: orders-audit receives 4, orders-fulfilment receives 3 (the three order_created events), and orders-highvalue receives 1 (the order with total 900). If audit shows 4 but fulfilment shows 0, suspect the 15-minute propagation first and the policy second. We did not run this exact script against a live account for this article; it follows the SNS and SQS API references and the developer guide, so run it with a throwaway account and read the output before you trust it.

FIFO, archive and replay

A FIFO topic is a different object and cannot be converted from a standard topic. The name must end in .fifo, every publish needs a MessageGroupId, and you need either a MessageDeduplicationId or ContentBasedDeduplication enabled. Archiving and replay exist only on FIFO topics.

fifo_arn = sns.create_topic(
    Name="orders-ledger.fifo",
    Attributes={
        "FifoTopic": "true",
        "ContentBasedDeduplication": "true",
        "ArchivePolicy": json.dumps({"MessageRetentionPeriod": "7"}),
    },
)["TopicArn"]

sns.publish(
    TopicArn=fifo_arn,
    Message=json.dumps({"order": "o-1", "event": "created"}),
    MessageGroupId="customer-42",
)

The ArchivePolicy value has a single property, MessageRetentionPeriod, in days, from 1 to 365. Setting it to an empty JSON object turns archiving off and deletes the archive. You cannot delete a topic with an active archive policy, so cleanup has a required order. Replay is driven by the subscriber through the ReplayPolicy subscription attribute, which selects a time window and re-delivers archived messages to that endpoint. GetTopicAttributes returns BeginningArchiveTime, the oldest timestamp you can replay from. If the topic is encrypted, the KMS key policy must grant sns.amazonaws.com kms:Decrypt and kms:GenerateDataKey, or replay fails.

The 7-day retention here is a deliberate choice for the tutorial. Archiving is billed on stored data and time, so a long retention on a busy topic is a real line item.

When it breaks

The queue stays empty and no error appears. Check the SQS access policy first: if SNS cannot write, the delivery fails on the SNS side, and without a subscription DLQ you will never see it. The CloudWatch metric NumberOfNotificationsFailed on the topic is where it shows up, and AWS recommends alarming on it.

A message was delivered to audit but not to the queue you filtered. Filter matching is case-sensitive, attribute filtering only compares String, String.Array, and Number types, and Binary attributes are ignored. If you used attribute scope but put the field in the body, nothing matches. Check FilterPolicyScope.

InvalidParameter when you raise MaximumMessageSize. The topic has an HTTP, email, SMS, or push subscription, or more than 100 subscriptions. Large-message topics accept only SQS, Lambda, and Firehose.

HTTP subscriber gets the message, returns a 400, and never sees it again. A 4xx is a permanent failure and is not retried. Only 5xx and 429 are retried, so return those for transient trouble and keep 4xx for bad requests.

Subscribe fails with a limit error after you created many filter policies. The quotas are 200 policies per topic and 10,000 per account, and the failure is at Subscribe or SetSubscriptionAttributes, while the plain SQS subscription itself can still be created. Raise it through Service Quotas.

Cleanup

Order matters because of the archive. Disable archiving first by setting ArchivePolicy to an empty JSON object, which deletes the archived messages, then delete both topics, then the queues.

sns.set_topic_attributes(TopicArn=fifo_arn, AttributeName="ArchivePolicy",
                         AttributeValue="{}")
sns.delete_topic(TopicArn=fifo_arn)

for sub in sns.list_subscriptions_by_topic(TopicArn=topic_arn)["Subscriptions"]:
    sns.unsubscribe(SubscriptionArn=sub["SubscriptionArn"])
sns.delete_topic(TopicArn=topic_arn)

for url, _ in queues.values():
    sqs.delete_queue(QueueUrl=url)

Deleting a topic removes its subscriptions anyway, so the explicit unsubscribe loop is belt and braces. Confirm in the SNS and SQS consoles that no topics or queues remain, and the bill from this exercise is zero.

The takeaway

SNS makes delivery someone else's problem right up to the moment delivery fails, and then it becomes yours, silently. The service has three decisions worth making on purpose: put SQS between SNS and anything that can be slow, attach a dead-letter queue to every subscription, and filter at the subscription instead of in your consumers. Get those three right and SNS is the least eventful part of your architecture, which is the whole job.

Sources