The previous lesson left MercadoFresco half decoupled. cola-mercadofresco-pedidos works, the
confirmation is down to 400 ms and the ERP can fall over without taking sales with it, but the queue is
a point-to-point channel: each message is processed by one consumer and then disappears. With the
warehouse, email, delivery and analytics all interested in the same fact, the shop has ended up sending
four messages to four different queues. It knows again who its consumers are, which is exactly what we
wanted to avoid. And when marketing asks to hear about orders for its loyalty programme, somebody will
have to touch the shop's code, deploy it and test it.
Amazon SNS (Simple Notification Service) is AWS's publish/subscribe service. The shop publishes once to a topic —"order PED-084417 has been confirmed"— and SNS delivers a copy to every subscriber. Who is subscribed is a configuration detail, not a code detail. Adding marketing goes from being a deployment to being a one-line command.
In this lesson Luis creates mercadofresco-pedido-confirmado, builds the SNS→SQS fan-out pattern with
the warehouse, email and analytics queues, filters by attribute so that the 24-hour delivery queue only
receives what concerns it, and finally understands what the alertas-mercadofresco topic we have been
using since module 5 really was.
Cost warning. SNS charges per publication and per delivery: the first million API requests are free, and deliveries to SQS and Lambda are free too. What really costs money is SMS (around 0.06 USD per message in Spain) and, to a lesser extent, email. A test loop sending SMS can generate a surprising bill in minutes. At the end you have the cleanup. All data is fictitious.
Contents
- Publish/subscribe versus the point-to-point queue
- Topics, publishers and subscribers
- Subscription types and their guarantees
- Subscription confirmation and its traps
- The SNS→SQS fan-out pattern
- Why a queue in the middle and not a Lambda directly
- Building MercadoFresco's fan-out with the CLI and boto3
- Filtering by attribute and by message body
- Retries, delivery policies and SNS's own DLQ
- Messages with a structure per protocol
- FIFO topics and how they fit with FIFO queues
- Security: topic policy, encryption and cross-account access
alertas-mercadofrescorevisited, SMS and transactional email- Metrics and debugging deliveries
- Cost and cleanup
- Common mistakes and tips
- Exercises
- Conclusion
Publish/subscribe versus the point-to-point queue
The difference between SQS and SNS is not one of technology, it is one of who knows whom.
| Queue (SQS) | Topic (SNS) | |
|---|---|---|
| Model | Point to point | Publish/subscribe |
| Consumers per message | One (whoever takes it) | All the subscribed ones |
| Who knows whom | The producer knows the queue | The publisher knows nobody |
| Storage | Durable for up to 14 days | None: deliver and forget |
| If the destination is down | The message waits | Retries and then it is lost |
| Consumption model | Polling (pull) | Push |
| Adding a consumer | Requires the producer to send there too | One subscription, no change to the producer |
The row to memorise is the storage one: SNS stores nothing. A topic is a router, not a mailbox. If you publish to a topic with no subscribers, the message evaporates with no error and no warning. And if the only subscriber is a dead HTTP endpoint, SNS retries according to its delivery policy and, once exhausted, discards the message. This is reason number one why the correct pattern for important work is SNS plus SQS, not SNS on its own.
flowchart LR
subgraph before["Before: the shop knows its four consumers"]
T1[Shop] --> QA1[(warehouse queue)]
T1 --> QB1[(email queue)]
T1 --> QC1[(analytics queue)]
T1 --> QD1[(delivery queue)]
end
subgraph after["After: the shop publishes a fact and walks away"]
T2[Shop] -->|1 Publish| TEMA{{mercadofresco-pedido-confirmado}}
TEMA --> QA2[(cola-mercadofresco-almacen)]
TEMA --> QB2[(cola-mercadofresco-correo)]
TEMA --> QC2[(cola-mercadofresco-analitica)]
TEMA -.->|new, without touching the shop| QE2[(cola-mercadofresco-fidelizacion)]
end
style TEMA fill:#cfe2ff
style QE2 fill:#d4edda
The conceptual change is that the shop stops giving orders ("put this in the warehouse queue") and starts communicating facts ("an order has been confirmed"). A fact has no addressee: whoever is interested subscribes. That inversion —from command to event— is the basis of the event-driven architecture we will go deeper into in 07-03.
Topics, publishers and subscribers
A topic is a named logical channel with an ARN. Creating one is instant and free:
export PROFILE="--profile mercadofresco-dev --region eu-west-1"
export TAGS='Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion Key=Componente,Value=integracion Key=Propietario,Value=marta Key=CentroCoste,Value=tecnologia'
aws sns create-topic --name mercadofresco-pedido-confirmado \
--attributes KmsMasterKeyId=alias/mercadofresco-datos \
--tags $TAGS $PROFILE
# → arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmadoThere are two classes of topic:
| Standard topic | FIFO topic | |
|---|---|---|
| Name | Free | Must end in .fifo |
| Ordering | Not guaranteed | Strict by MessageGroupId |
| Delivery | At least once | Exactly once (5-minute window) |
| Throughput | Practically unlimited | 300 publications/s (3,000 in high throughput) |
| Allowed subscribers | All | SQS FIFO queues only (and Lambda since 2023) |
| Publication price | 0.50 USD/million | 0.30 USD/million + 0.017 USD/GB |
A publisher calls Publish with the topic ARN; it only needs sns:Publish. A subscriber is an
endpoint registered on the topic through Subscribe, with a protocol and an address. The limit per
topic is 12.5 million subscriptions, so in practice it does not exist.
Subscription types and their guarantees
| Protocol | Endpoint | Retries | Use case in MercadoFresco |
|---|---|---|---|
sqs |
Queue ARN | Up to 100,000 s (~23 h) | The main one: reliable asynchronous work |
lambda |
Function ARN | Up to 100,000 s | Trivial reaction with no need to buffer |
https / http |
URL | Configurable, up to 23 h with backoff | Webhook to the partner's ERP |
email / email-json |
Address | No useful retries | Warnings to Marta from alertas-mercadofresco |
sms |
E.164 number | Depends on the carrier | Urgent notice to the delivery driver |
application |
Mobile platform endpoint | Yes | Push notification to the delivery app |
firehose |
Delivery stream ARN | Yes | Dumping events to mercadofresco-registros-web |
The guarantees are not the same. Deliveries to internal AWS destinations (SQS, Lambda, Firehose) are the most reliable: long retries, no dependency on the public network and a DLQ available. HTTP/S deliveries depend on your endpoint being alive, answering a 2xx in under 15 seconds and coping with bursts. Those for email and SMS are, in practical terms, "at most once": if the provider rejects them, SNS can do little about it, and they suit nothing that has to happen no matter what.
Subscription confirmation and its traps
The protocols that point outside AWS —HTTP/S and email— require confirmation. SNS sends the
endpoint a SubscriptionConfirmation message with a SubscribeURL, and until somebody visits it the
subscription stays in PendingConfirmation and receives nothing.
{
"Type": "SubscriptionConfirmation",
"MessageId": "8f21a3c4-...",
"Token": "2336412f37...",
"TopicArn": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado",
"Message": "You have chosen to subscribe to the topic ...",
"SubscribeURL": "https://sns.eu-west-1.amazonaws.com/?Action=ConfirmSubscription&...",
"Timestamp": "2026-08-02T18:41:07.000Z",
"SignatureVersion": "1",
"Signature": "EXAMPLEpH+..."
}The traps, all of them seen in production:
- The HTTP endpoint receives
SubscriptionConfirmationand does not know what to do with it. Your handler expectsType: "Notification"and returns 400. The subscription is never confirmed and nobody notices until an event goes missing. The handler must tell the three types apart:SubscriptionConfirmation,NotificationandUnsubscribeConfirmation. - Confirming without verifying the signature. Anybody who knows your URL can send you a JSON with a
SubscribeURLpointing at their own topic and, if you visit it blindly, you end up subscribed to somebody else's topic. Always verifySignaturewith the certificate fromSigningCertURL—and check that that domain issns.<region>.amazonaws.com—. - The confirmation email ends up in spam. The SNS notice arrives from
no-reply@sns.amazonaws.com; corporate filters block it frequently. - The token expires after 3 days. Once that deadline passes you have to subscribe again.
AuthenticateOnUnsubscribe. Without this attribute, anybody with the unsubscribe link can unsubscribe the endpoint. Set it totrueon production HTTP subscriptions.
Subscriptions to SQS, Lambda and Firehose do not require confirmation when created by a principal in the same account with sufficient permissions: they confirm themselves. Another reason to prefer them.
The SNS→SQS fan-out pattern
This is the most used integration pattern in AWS and the one MercadoFresco adopts as its standard: one topic that several queues subscribe to, and one consumer per queue.
flowchart TD
T[Shop: Publish] --> TEMA{{mercadofresco-pedido-confirmado}}
TEMA -->|no filter| QA[(cola-mercadofresco-almacen)]
TEMA -->|no filter| QB[(cola-mercadofresco-correo)]
TEMA -->|no filter| QC[(cola-mercadofresco-analitica)]
TEMA -->|filter franja_entrega = 24h| QD[(cola-mercadofresco-reparto-24h)]
QA --> CA[ERP consumer] --> DA[/Warehouse ERP/]
QB --> CB[Email Lambda] --> DB[/SMTP provider/]
QC --> CC[Analytics consumer] --> DC[(Redshift)]
QD --> CD[Delivery Lambda] --> DD[/Delivery driver API/]
QA -.->|4 failures| DLQA[(mercadofresco-almacen-fallidas)]
QB -.->|4 failures| DLQB[(mercadofresco-correo-fallidas)]
style TEMA fill:#cfe2ff
style DLQA fill:#f8d7da
style DLQB fill:#f8d7da
Each consumer moves at its own pace. The ERP one can run slowly and pile up 4,000 messages without the email one noticing. If the analytics consumer has a bug and has to be stopped for two hours, its messages wait in its queue and are processed afterwards: nobody else is affected.
Why a queue in the middle and not a Lambda directly
SNS can invoke Lambda functions directly, and it is tempting to save yourself the queue. It is a mistake as soon as the work matters. Three reasons:
1. Retries and durability. If SNS invokes a Lambda and the function fails, SNS retries according to
its delivery policy and then discards the message (or sends it to its DLQ, if you configured one).
With a queue in between, the message is stored for up to 14 days, the consumer retries it as many times
as you say and it ends up in a DLQ you can inspect and reprocess with start-message-move-task. The
queue turns an ephemeral event into persistent pending work.
2. Buffering and backpressure. On Friday at 18:00, 900 orders/hour arrive in bursts. With
SNS→Lambda, Lambda scales as fast as the invocations arrive: if behind it there is Aurora with 200
connections or SMTP with its sending limit, that peak propagates intact. With SNS→SQS→Lambda, the queue
absorbs the burst and MaximumConcurrency (07-01) sets the rate at which it drains.
3. Failure isolation. With four Lambdas subscribed to the topic and one of them throttled by the account's concurrency limit, its invocations fail and are lost. With four queues, the problem stays contained in the affected queue.
| Criterion | SNS → Lambda | SNS → SQS → consumer |
|---|---|---|
| Durability of the work | None once retries are exhausted | Up to 14 days |
| Reprocessing after fixing the fault | Impossible | Redrive from the DLQ |
| Control of the pace | None | MaximumConcurrency, batch size |
| Isolation between consumers | Low | High |
| Added latency | ~0 | Tens of ms |
| Pieces to maintain | 1 | 2 |
When SNS→Lambda directly is a good idea: trivial, idempotent reactions with no consequences if one is lost —refreshing a cache, writing a metric— and when minimum latency is a requirement. For anything that is a sale, a charge or a notice to a partner, put a queue in between.
Building MercadoFresco's fan-out with the CLI and boto3
The step that is forgotten most often is the queue policy: SNS is not an IAM principal in your account, so it needs explicit permission on every destination queue.
{
"Version": "2012-10-17",
"Statement": [{
"Sid": "PermitirEntregaDesdeTemaPedidoConfirmado",
"Effect": "Allow",
"Principal": { "Service": "sns.amazonaws.com" },
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-almacen",
"Condition": {
"ArnEquals": {
"aws:SourceArn": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado"
}
}
}]
}Without the aws:SourceArn condition, any SNS topic in the world could write to your queue. It is
the same protection pattern as in 07-01 with S3.
With that in place, the full setup:
TOPIC=arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado
for C in almacen correo analitica; do
QUEUE_ARN=arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-$C
aws sqs set-queue-attributes \
--queue-url https://sqs.eu-west-1.amazonaws.com/111122223333/cola-mercadofresco-$C \
--attributes Policy="$(sed "s|COLA_ARN|$QUEUE_ARN|" politica-cola.json | tr -d '\n')" $PROFILE
aws sns subscribe --topic-arn $TOPIC --protocol sqs --notification-endpoint $QUEUE_ARN \
--attributes RawMessageDelivery=true $PROFILE
doneRawMessageDelivery=true deserves a paragraph of its own. By default, SNS wraps your message in
a JSON envelope with metadata, and the original body ends up as an escaped string inside the Message
field:
{
"Type": "Notification",
"MessageId": "1d9a...",
"TopicArn": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado",
"Message": "{\"pedido_id\":\"PED-2026-084417\",\"importe_eur\":48.20}",
"Timestamp": "2026-08-02T18:41:07.123Z",
"MessageAttributes": { "franja_entrega": { "Type": "String", "Value": "24h" } }
}This forces the consumer to do a double json.loads and to read the attributes from a different place
than it would if the message arrived straight at the queue. With RawMessageDelivery=true, the queue
receives the body as it is and the SNS attributes become native SQS attributes: the same consumer
works whether the message comes from the topic or was sent directly to the queue. Always enable it on
SQS and Firehose subscriptions.
The publisher then looks like this:
import json, os
from datetime import datetime, timezone
import boto3
sns = boto3.client("sns", region_name="eu-west-1")
TOPIC = os.environ["ARN_TEMA_PEDIDO_CONFIRMADO"]
def publish_order_confirmed(order):
"""The shop communicates a fact. It neither knows nor cares who hears about it."""
response = sns.publish(
TopicArn=TOPIC,
Subject=f"Order confirmed {order['pedido_id']}", # only email and HTTP use it
Message=json.dumps({
"version": 1,
"pedido_id": order["pedido_id"],
"cliente_id": order["cliente_id"],
"importe_eur": float(order["importe_eur"]),
"franja_entrega": order["franja_entrega"], # "24h" | "estandar"
"provincia": order["provincia"],
"lineas": order["lineas"],
"confirmado_en": datetime.now(timezone.utc).isoformat(),
}, ensure_ascii=False),
MessageAttributes={
# These attributes are the ones the filter policies evaluate.
"tipo_evento": {"DataType": "String", "StringValue": "PedidoConfirmado"},
"franja_entrega": {"DataType": "String", "StringValue": order["franja_entrega"]},
"provincia": {"DataType": "String", "StringValue": order["provincia"]},
"importe_eur": {"DataType": "Number", "StringValue": str(order["importe_eur"])},
},
)
return response["MessageId"]Notice that the fields used for filtering are duplicated: in the body, because the consumer needs them, and in the attributes, because by default SNS only looks there. It is a deliberate and cheap duplication.
For high volumes, publish_batch accepts 10 messages per call with the same trap as
send_message_batch: it returns 200 with a Failed list that has to be inspected.
Filtering by attribute and by message body
A filter policy is a JSON attached to the subscription. If the message does not match, SNS does not deliver it to that subscriber —and does not charge for it—. It avoids the antipattern of "everyone receives everything and each consumer discards the rest", which wastes invocations and fills the queues.
Marta wants cola-mercadofresco-reparto-24h to receive only orders in the 24-hour slot:
aws sns set-subscription-attributes \
--subscription-arn arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado:9c1f... \
--attribute-name FilterPolicy \
--attribute-value '{"franja_entrega":["24h"]}' $PROFILEThe filter language supports far more than equality:
| Operator | Example | Meaning |
|---|---|---|
| Exact match | {"franja_entrega": ["24h", "express"]} |
Either of the two will do |
anything-but |
{"provincia": [{"anything-but": ["Baleares", "Canarias"]}]} |
Everything but the islands |
prefix |
{"tipo_evento": [{"prefix": "Pedido"}]} |
PedidoConfirmado, PedidoCancelado… |
suffix |
{"sku": [{"suffix": "-BIO"}]} |
Organic products |
numeric |
{"importe_eur": [{"numeric": [">=", 150]}]} |
Large orders |
| Numeric range | {"importe_eur": [{"numeric": [">", 50, "<=", 200]}]} |
Between 50 and 200 € |
exists |
{"cupon": [{"exists": true}]} |
Only if the attribute is present |
| OR combination | {"franja_entrega": ["24h"], "importe_eur": [{"numeric": [">=", 150]}]} |
See below |
The rule that confuses people most: within one attribute the list is an OR; between different
attributes it is an AND. The last row of the table demands the 24h slot and also an amount ≥
150. To express an OR between different attributes you need $or:
Filtering by message body. Since 2022 SNS can filter by looking inside the body's JSON, which
removes the duplication of fields. You enable it by setting FilterPolicyScope to MessageBody:
aws sns set-subscription-attributes --subscription-arn $SUB \
--attribute-name FilterPolicyScope --attribute-value MessageBody $PROFILE
aws sns set-subscription-attributes --subscription-arn $SUB \
--attribute-name FilterPolicy \
--attribute-value '{"franja_entrega":["24h"],"lineas":{"sku":[{"prefix":"PESC-"}]}}' $PROFILEThe policy can navigate nested objects and arrays. Two limits that matter: the body must be valid JSON —if it is not, the subscription receives nothing and the failure is silent— and a subscription has a single filter scope, either attributes or body, never both.
| Attribute filter | Body filter | |
|---|---|---|
| Duplicating fields | Yes | No |
| Nesting and arrays | No | Yes |
| If the body is not JSON | Works the same | Delivers nothing |
| Publication cost | Same | Same |
| Recommendation | Stable contracts and flat fields | Rich filters over the detail |
MercadoFresco uses attributes for basic routing (event type, slot, province) and the body for
rich cases, such as alerting the cold chain team when one of the lines starts with PESC-.
Retries, delivery policies and SNS's own DLQ
When SNS cannot deliver, it retries according to the protocol's delivery policy. For SQS, Lambda and Firehose it is predefined and generous: immediate, pre-backoff, exponential backoff and post-backoff phases, up to about 23 hours in total. For HTTP/S it is configurable:
{
"healthyRetryPolicy": {
"minDelayTarget": 5,
"maxDelayTarget": 300,
"numRetries": 50,
"numNoDelayRetries": 0,
"numMinDelayRetries": 3,
"numMaxDelayRetries": 10,
"backoffFunction": "exponential"
},
"throttlePolicy": { "maxReceivesPerSecond": 20 }
}maxReceivesPerSecond is the backpressure towards the partner's ERP: even if MercadoFresco publishes
900 messages in a minute, SNS will not send it more than 20 per second. It is the only way to protect
an HTTP endpoint you cannot scale.
The SNS DLQ is configured per subscription, not per topic, with the RedrivePolicy attribute.
It collects the messages that exhausted the retries towards that particular subscriber:
aws sns set-subscription-attributes --subscription-arn $SUB_ERP \
--attribute-name RedrivePolicy \
--attribute-value '{"deadLetterTargetArn":"arn:aws:sqs:eu-west-1:111122223333:mercadofresco-sns-fallidas"}' \
$PROFILETwo DLQs coexist in the same architecture and must be told apart, because they do not mean the same:
| SNS subscription DLQ | SQS queue DLQ | |
|---|---|---|
| What it collects | What SNS could not deliver | What the consumer could not process |
| Typical cause | Endpoint down, permissions, deleted queue | Invalid data, dependency down |
| Configured on | The subscription | The source queue |
| Signal | Infrastructure or permissions problem | Application or data problem |
Both need an alarm on ApproximateNumberOfMessagesVisible towards alertas-mercadofresco.
Messages with a structure per protocol
The same event does not read the same way in a 160-character SMS as it does in a queue. With
MessageStructure="json", a single Publish carries a different payload per protocol:
sns.publish(
TopicArn=ALERTS_TOPIC,
Subject="Order queue running late",
MessageStructure="json",
Message=json.dumps({
"default": "The queue cola-mercadofresco-pedidos is more than 5 minutes behind.",
"email": ("Alarm: mercadofresco-pedidos-cola-retrasada\n\n"
"ApproximateAgeOfOldestMessage > 300 s for 3 periods.\n"
"Dashboard: https://console.aws.amazon.com/cloudwatch/home#dashboards:name=mercadofresco-produccion"),
"sms": "MercadoFresco: order queue running late >5 min",
"sqs": json.dumps({"alarma": "cola-retrasada", "cola": "cola-mercadofresco-pedidos"}),
}),
)The default key is mandatory and is used for any protocol without an entry of its own. If it is
missing, Publish returns InvalidParameter. Only email and HTTP use the Subject; it is ignored on
SQS and Lambda, and it is limited to 100 ASCII characters.
About attributes: a message accepts up to 10, and their size counts towards the message's 256 KiB
limit. With MessageStructure="json", each per-protocol payload also counts against that total limit.
FIFO topics and how they fit with FIFO queues
A FIFO topic guarantees end-to-end ordering and deduplication, but it can only deliver to SQS FIFO queues (and, since 2023, to Lambda functions). It does not support HTTP, email or SMS. It fits the stock queue we created in 07-01:
aws sns create-topic --name mercadofresco-stock-movimientos.fifo \
--attributes FifoTopic=true,ContentBasedDeduplication=false \
--tags $TAGS $PROFILE
aws sns subscribe --topic-arn arn:aws:sns:eu-west-1:111122223333:mercadofresco-stock-movimientos.fifo \
--protocol sqs \
--notification-endpoint arn:aws:sqs:eu-west-1:111122223333:cola-mercadofresco-stock.fifo \
--attributes RawMessageDelivery=true $PROFILEWhen publishing you have to supply MessageGroupId and, if there is no content-based deduplication,
MessageDeduplicationId:
sns.publish(
TopicArn=STOCK_TOPIC,
Message=json.dumps(movement, ensure_ascii=False),
MessageGroupId=movement["sku"], # ordering per product
MessageDeduplicationId=f"{movement['pedido_id']}:{movement['sku']}:{movement['motivo']}",
MessageAttributes={"motivo": {"DataType": "String", "StringValue": movement["motivo"]}},
)The MessageGroupId is propagated to the queue, so the ordering is preserved along the whole chain.
And attribute filtering works on FIFO topics too, which lets one queue receive only the reservations
and another one all the movements.
Security: topic policy, encryption and cross-account access
The topic policy controls who publishes and who subscribes. By default, only the account owner. This one tightens it: the shop publishes, and only SQS subscriptions can be created.
{
"Version": "2012-10-17",
"Id": "politica-mercadofresco-pedido-confirmado",
"Statement": [
{
"Sid": "SoloLaTiendaPublica",
"Effect": "Allow",
"Principal": { "AWS": "arn:aws:iam::111122223333:role/rol-mercadofresco-tienda" },
"Action": "sns:Publish",
"Resource": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado"
},
{
"Sid": "SuscripcionesSoloDeColas",
"Effect": "Allow",
"Principal": { "AWS": "111122223333" },
"Action": "sns:Subscribe",
"Resource": "arn:aws:sns:eu-west-1:111122223333:mercadofresco-pedido-confirmado",
"Condition": { "StringEquals": { "sns:Protocol": "sqs" } }
}
]
}The sns:Protocol condition is a practical safeguard: it stops somebody subscribing their personal
email address to a topic that carries customer addresses. There is also sns:Endpoint to restrict to
specific endpoints.
Encryption. KmsMasterKeyId=alias/mercadofresco-datos encrypts the messages at rest inside SNS.
Two warnings: the publisher needs kms:GenerateDataKey* and kms:Decrypt on the key, and if an AWS
service publishes to the topic —CloudWatch when it fires an alarm, S3 when an object is uploaded— the
key policy must allow it explicitly. It is the usual cause of alarms that stop warning anybody
right after encryption is switched on.
Cross-account access. The delivery partner has its own AWS account and wants to consume the events.
They are not given credentials: their account is added to the topic policy with sns:Subscribe, and
they create in their account a queue whose policy accepts our topic. Neither of the two parties
gets access to the other's infrastructure. It is the same least-privilege principle from 04-01 applied
to integration.
alertas-mercadofresco revisited, SMS and transactional email
Since module 5 we have been writing --alarm-actions arn:aws:sns:...:alertas-mercadofresco without
fully explaining what was going on. Now it is clear: a CloudWatch alarm is an SNS publisher. When
it moves to ALARM, it publishes a JSON with AlarmName, NewStateValue, NewStateReason and the
metric's dimensions. The topic has Marta's email subscribed, and it could have subscribed —without
touching a single alarm— a queue that records the history, a Lambda that opens a ticket or an HTTPS
endpoint towards the on-call tool.
That is exactly the value of the model: CloudWatch does not know who hears about its alarms. Adding a destination does not require modifying 40 alarms, only creating a subscription.
An immediate improvement now that we know about filtering: subscribe the on-call SMS with a policy that only lets the critical things through.
With FilterPolicyScope=MessageBody, Marta receives an SMS only for order alarms that enter the
ALARM state, while the email keeps receiving everything, including the returns to OK.
About SMS and email. SNS is for operational notices to known people: an SMS to the on-call
engineer, an email to Marta. It is not for transactional email or for campaigns: there are no
templates, no open tracking, no bounce handling, no sender reputation control, and the sender is
no-reply@sns.amazonaws.com. MercadoFresco's order confirmation email is sent with Amazon SES,
which does offer your own domain, DKIM, templates and deliverability metrics; the Lambda subscribed to
cola-mercadofresco-correo calls SES, not SNS. For bulk SMS, SNS also requires leaving the sandbox
and registering the sender with the carrier.
Metrics and debugging deliveries
| Metric | What it indicates | Action |
|---|---|---|
NumberOfMessagesPublished |
Volume published | Traffic reference |
NumberOfNotificationsDelivered |
Successful deliveries | Compare with published × subscribers |
NumberOfNotificationsFailed |
Failed deliveries | Immediate alarm |
NumberOfNotificationsFilteredOut |
Discarded by filter policy | If it is 100 %, the filter is wrong |
NumberOfNotificationsFilteredOut-NoMessageAttributes |
The message carried no attributes | Publisher that forgot the attributes |
NumberOfNotificationsFilteredOut-InvalidAttributes |
Attributes with the wrong type | Numbers sent as strings, etc. |
PublishSize |
Size of the messages | Close to 256 KiB → use S3 |
SMSMonthToDateSpentUSD |
SMS spend so far this month | Cost alarm |
The three FilteredOut metrics are the ones that save the most time. The classic symptom —"the queue
receives nothing and there is no error"— is almost always a filter: either the publisher does not send
the attributes, or it sends them with the wrong type. A Number sent as a String does not match a
numeric filter, and SNS discards it without saying a word.
aws cloudwatch put-metric-alarm --alarm-name mercadofresco-sns-entregas-fallidas \
--namespace AWS/SNS --metric-name NumberOfNotificationsFailed \
--dimensions Name=TopicName,Value=mercadofresco-pedido-confirmado \
--statistic Sum --period 300 --evaluation-periods 1 --threshold 0 \
--comparison-operator GreaterThanThreshold --treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco $PROFILETo debug failed deliveries you need to enable the delivery status logs, which are not on by
default: you configure a role with write permission on CloudWatch Logs and a sampling percentage
(SQSSuccessFeedbackSampleRate, HTTPSuccessFeedbackSampleRate…). With them, the endpoint's real
response codes appear in Logs, which is the only thing that lets you tell a 403 from permissions apart
from a 500 from the partner. And since every Publish is recorded in trail-mercadofresco (05-03), you
can always check whether the event was published at all.
Cost and cleanup
| Item | Price (eu-west-1) | MercadoFresco |
|---|---|---|
| Publications | 1 M free, then 0.50 USD/M | 240,000/month → 0 USD |
| Deliveries to SQS and Lambda | Free | 960,000 → 0 USD |
| HTTP/S deliveries | 0.60 USD/M | 240,000 → 0.14 USD |
| Email deliveries | 2.00 USD/100,000 | 300/month → 0.01 USD |
| SMS to Spain | ~0.06 USD/message | 40/month → 2.40 USD |
| Data transferred | 0.09 USD/GB out | ~0.4 GB → 0.04 USD |
| Total | ~2.60 USD/month |
What has to be watched is SMS: a test loop sending 5,000 messages costs 300 USD in a few minutes.
Always set MonthlySpendLimit in the account's SMS preferences and an alarm on
SMSMonthToDateSpentUSD. Note as well that fan-out multiplies deliveries: publishing 240,000 times
with four subscribers is 960,000 deliveries, free towards SQS but chargeable towards HTTP.
aws sns list-subscriptions-by-topic --topic-arn $TOPIC \
--query 'Subscriptions[].SubscriptionArn' --output text $PROFILE | \
xargs -n1 -I{} aws sns unsubscribe --subscription-arn {} $PROFILE
aws sns delete-topic --topic-arn $TOPIC $PROFILE
aws sns delete-topic --topic-arn arn:aws:sns:eu-west-1:111122223333:mercadofresco-stock-movimientos.fifo $PROFILEDeleting the topic deletes neither the subscribed queues nor their messages: clean those separately with the 07-01 commands. And beware: deleting a topic does not remove unconfirmed email subscriptions.
Common Mistakes and Tips
Publishing to a topic with no subscribers. Publish returns 200 and a MessageId, and the message
evaporates. No error, no failure metric. Check NumberOfNotificationsDelivered after any deployment
that touches subscriptions.
Forgetting the queue policy. The subscription is created without complaint, but SNS cannot write.
Messages are lost and NumberOfNotificationsFailed goes up. It is the number one fan-out failure.
A queue policy without aws:SourceArn. It works, but it leaves your queue open to any SNS topic.
Not enabling RawMessageDelivery. The consumer receives the SNS envelope, needs a double
json.loads and the attributes turn up where it is not looking. Always enable it on SQS subscriptions.
Filters that discard everything silently. The publisher does not send the attributes, or sends a
number as a String when the filter uses numeric. Symptom: zero messages and zero errors. Look at
NumberOfNotificationsFilteredOut and its two variants. And do not confuse OR and AND: within one
attribute, list = OR; between different attributes, AND; for an OR between attributes you need $or.
Subscribing Lambdas directly for important work, or assuming that SNS stores the messages. It does not: a consumer that is down during a deployment permanently loses everything published in that window if there is no queue in between.
Encrypting the topic and forgetting the key policy. CloudWatch alarms stop publishing and nobody finds out —because the very mechanism that warns you is the one that has failed—.
Tip: name topics after the fact, not after the destination. mercadofresco-pedido-confirmado, not
mercadofresco-avisar-almacen. The name is part of the contract and should not age when the consumers
change.
Tip: put version in the body from the very first message. When a field has to be added a year
from now, older consumers will be able to ignore it safely.
Tip: filter at the topic, not at the consumer. A consumer that discards 90 % of what it receives pays for invocations, queue requests and complexity for nothing.
Exercises
Exercise 1: the marketing consumer
Marketing wants to process confirmed orders for its loyalty programme, but it is only interested in those of 60 € or more or those from customers in Catalonia, and its system is an HTTPS endpoint that copes with 5 requests per second and suffers outages of up to 40 minutes.
Design the integration: (a) what you subscribe to the topic and why; (b) the exact filter policy in JSON; (c) what you configure to protect its endpoint from the Friday peak; (d) which DLQs are involved and what each one means; (e) what changes in the shop's code.
Exercise 2: diagnosing the silent fan-out
Luis deploys the fan-out on a Thursday. On Friday morning, Marta sees:
NumberOfMessagesPublished 8,400; NumberOfNotificationsDelivered 16,800;
NumberOfNotificationsFailed 8,400; NumberOfNotificationsFilteredOut 0. The warehouse and email
queues are receiving normally; the analytics one is empty. cola-mercadofresco-analitica exists, its
consumer works if messages are sent to it by hand, and the subscription shows as Confirmed.
Explain: (a) what exactly is failing and how you deduce it from the numbers; (b) why the subscription is confirmed even though it does not work; (c) what command you would run to confirm the diagnosis; (d) the fix; (e) what alarm would have warned on Thursday afternoon.
Exercise 3: SNS or SQS
For each case, decide whether to use SNS, SQS or SNS→SQS, and justify it in two sentences: (a) the shop tells the warehouse to prepare a box; (b) three teams want to hear about order cancellations and a fourth is expected; (c) when a photo is uploaded the thumbnail has to be generated; (d) Marta has to be warned by SMS when the DLQ has messages; (e) stock movements have to be sent to two different systems keeping the ordering per SKU; (f) the shipping cost service has to be called to show the price on the checkout screen.
Solutions
Solution 1
(a) What gets subscribed is an SQS queue (cola-mercadofresco-fidelizacion), not the HTTPS
endpoint. The reason is the 40-minute outage: if SNS delivered directly over HTTP, we would depend on
the delivery policy holding out, and anything that failed once the retries were exhausted would be
lost. With a queue, the events wait up to 14 days and are processed when the system comes back. The
consuming is done by a Lambda subscribed to the queue that calls the marketing endpoint.
(b) Since the requirement is an OR between two different attributes, $or is needed:
{
"$or": [
{ "importe_eur": [{ "numeric": [">=", 60] }] },
{ "provincia": ["Barcelona", "Girona", "Lleida", "Tarragona"] }
]
}If the two attributes were written in the same object without $or, SNS would demand both conditions
at once and marketing would lose most of the events. Requirement: the shop must publish importe_eur
as a Number and provincia as a String.
(c) The protection is in the consumer, not in SNS: MaximumConcurrency=5 on the Lambda's event
source mapping, which limits the pace to what the endpoint can take; a small batch-size; and a queue
VisibilityTimeout ≥ 6 × the function's timeout. The queue absorbs the Friday peak and drains it at
5 requests per second. (If the endpoint had been subscribed directly instead of a queue, the equivalent
tool would be throttlePolicy.maxReceivesPerSecond in the delivery policy.)
(d) Two different DLQs. The SNS subscription DLQ would collect delivery failures from the topic
to the queue —badly set permissions, a deleted queue—; in practice it almost never fires because delivery
to SQS is very reliable, but it is worth having. The queue DLQ (mercadofresco-fidelizacion-fallidas,
with maxReceiveCount=4) collects what the Lambda could not process: if after 40 minutes of outage the
endpoint is still rejecting, those events are isolated there and reprocessed with redrive.
(e) Nothing. That is the result we were after: the shop already publishes the fact with all the attributes needed, and adding marketing means creating a queue, a queue policy, a subscription with its filter and a Lambda. Zero deployments of the shop's code.
Solution 2
(a) The numbers match exactly with one of the three subscriptions failing every single time.
With 8,400 publications and three subscribers there should be 25,200 deliveries; there are 16,800
successful (two subscribers × 8,400) and 8,400 failed (the third × 8,400). Since FilteredOut is 0,
it is not a filter problem: delivery is attempted and it fails. Combined with the fact that the
analytics queue exists and its consumer works, the diagnosis is clear: the policy on
cola-mercadofresco-analitica does not allow SNS to write to it. Almost certainly, Luis's
deployment loop failed when applying the policy on that queue, or applied it with the wrong
aws:SourceArn.
(b) Confirmed and functional are different things. SQS subscriptions within the same account
self-confirm on creation: SNS checks that the ARN is valid, not that it has write permission. The
permission is evaluated on every delivery, not on subscribing. That is why the state is Confirmed
and every publication fails with AccessDenied.
(c) aws sqs get-queue-attributes --queue-url ...cola-mercadofresco-analitica --attribute-names Policy to see whether there is a policy and whether the aws:SourceArn matches the topic ARN. As a
complement: enable the topic's delivery status logs and look in CloudWatch Logs for the real error code
of the failed deliveries, which will say AccessDenied unambiguously.
(d) Apply to the queue the same policy as to the other two, with Principal sns.amazonaws.com,
action sqs:SendMessage, Resource the analytics queue ARN and an ArnEquals condition on
aws:SourceArn with the topic ARN. Nothing else: the messages from the last few hours are already lost
—SNS does not store anything— and that is where you see why EventBridge's event archive and replay
(07-03) are so valuable.
(e) An alarm on the topic's NumberOfNotificationsFailed > 0, with a five-minute period, would
have warned within minutes of Thursday's deployment instead of the problem being discovered on Friday
morning. It is the minimum mandatory alarm for any SNS topic. As a complement, the analytics queue's
ApproximateNumberOfMessagesVisible sitting at 0 during working hours would also have been a clear
signal.
Solution 3
(a) SQS. It is an order with a single known addressee and it needs durability and retries. A topic adds nothing if there is only one interested party, and it would add one more piece to maintain.
(b) SNS→SQS. It is the canonical fan-out case: several parties interested in the same fact and one more expected. Each team with its own queue to isolate itself from the others' failures and pace.
(c) SQS. A single consumer and idempotent work. S3 can publish directly to the thumbnails queue; putting a topic in the middle would only make sense if another system needed to hear about the new photos.
(d) SNS. The destination is a person over a channel SNS supports natively. No queue is needed here: the CloudWatch alarm publishes and SNS delivers. Adding SQS would improve nothing, because an SMS is not reprocessed.
(e) FIFO topic → FIFO queues. Two destinations demand fan-out and the ordering per SKU demands
end-to-end FIFO. mercadofresco-stock-movimientos.fifo with MessageGroupId equal to the sku and
two FIFO queues subscribed, each with its own filter where appropriate.
(f) Neither: a synchronous call. The customer needs the shipping price now in order to decide. Neither queue nor topic: an HTTP call with a short timeout, bounded retries and a default value if the service does not answer. It is the reminder that asynchronous is not always better —it only is when the result is not part of the answer to the user—.
Conclusion
MercadoFresco has gone from a shop that sent four messages to four queues to a shop that publishes one
fact to mercadofresco-pedido-confirmado and walks away. The warehouse, email and analytics each have
their own queue subscribed with RawMessageDelivery=true, their own retries and their own DLQ;
cola-mercadofresco-reparto-24h receives only the orders in the urgent slot thanks to a filter policy;
the topic is encrypted with alias/mercadofresco-datos and its policy restricts who publishes and with
which protocols you can subscribe. Adding marketing is now creating a queue and a subscription, without
touching or deploying the shop's code.
Along the way the ideas that govern the pattern have become clear: that SNS stores nothing, which
makes the intermediate queue mandatory for any work that matters; that the queue provides durability,
buffering and failure isolation that a direct Lambda subscription cannot give; that filtering by
attribute or by body avoids the waste of everyone receiving everything, with the rule of OR within an
attribute and AND between attributes; that the subscription DLQ and the queue DLQ signal
different problems —delivery versus processing—; and that the alertas-mercadofresco topic from module
5 was this very mechanism all along, with CloudWatch as a publisher that does not know who is listening
to it.
But fan-out has a ceiling. SNS delivers the same message to every subscriber and only knows how to
filter with simple rules over whatever the publisher remembered to include. When MercadoFresco wants to
route by content for real —PedidoCancelado going to one place and StockBajo to another, all over the
same channel— it would have to create one topic per event type and go back to a map of subscriptions
that is hard to govern. There are also facts the shop does not publish that are equally interesting:
an EC2 instance changing state, Trusted Advisor spotting an underused resource, a deployment failing.
And something is missing that hurts today: when the analytics consumer was misconfigured, the events
from those hours were lost for ever, because a topic stores nothing.
In 07-03, "Amazon EventBridge", we take the leap from the topic to the event bus: a channel
where both applications and AWS services themselves publish, with rules that route according to the
full content of the event, input transformation so the destination is not coupled to the source format,
targets ranging from Lambda to an ERP API, scheduled tasks with cron, a direct connection to the
mercadofresco-carritos Streams through Pipes, and —the thing that would have saved the analytics
queue incident— archive and replay of everything published.
AWS Course
Module 1: Introduction to AWS
- What Is AWS?
- Setting Up Your AWS Account
- AWS Global Infrastructure
- The AWS Management Console
- AWS CLI and SDKs
Module 2: Core AWS Services
Module 3: Networking and Content Delivery
Module 4: Security and Identity
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager and Parameter Store
- AWS Shield
- AWS WAF
Module 5: Monitoring and Management
Module 6: Databases
Module 7: Application Integration
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Integration Patterns: Idempotency, Retries and Dead-Letter Queues
