Decoupling: SQS, SNS & EventBridge
Queues, pub/sub, and event-driven design.
Watch the pass in a busy restaurant. The waiter doesn't hand your order to a cook and stand there waiting for the plate. They clip a ticket to the rail and walk straight back out to the dining room. Cooks pull tickets at their own pace. When a tour bus empties into the car park, the rail gets long and the kitchen keeps its rhythm anyway. That rail is a queue, and it is the most useful resilience trick on AWS. It turns *everything must work right now* into *everything will be handled shortly*.
Now put names on the parts. The waiter is a producer, meaning any component that hands off work. The cook is a consumer, the component that does the work. The ticket is a message, a small piece of text describing one unit of work. Two components are tightly coupled when the producer calls the consumer directly and waits for an answer: a slow consumer stalls the producer, a dead one breaks it, and a rush hits both at the same moment. They are loosely coupled when something holds the work in between, so each side can crash, scale, and be redeployed on its own schedule. AWS gives you three managed ways to do that, and the exam expects you to tell them apart on sight: SQS (Simple Queue Service, for queues), SNS (Simple Notification Service, for publish and subscribe), and EventBridge (for routing events).
SQS: the rail that holds the tickets
Amazon SQS (Simple Queue Service) is that rail, run for you. Producers call SendMessage. SQS writes the message down in more than one Availability Zone (separate data centre campuses inside one AWS region), so losing a building doesn't lose your orders. Consumers ask for work with ReceiveMessage, and nothing is ever pushed at them. That pull model is the whole reason a queue can grow to a million messages without anything toppling over: the backlog sits in SQS, not in your application's memory. Messages stick around for 4 days by default, and you can set that anywhere from 60 seconds to 14 days, so a consumer that dies at 2am is an inconvenience rather than lost revenue. Queues come in two shapes. Standard queues take nearly unlimited throughput, deliver *at least once*, and make a best effort at keeping order. FIFO queues (first in, first out) hold strict order inside each message group and process each message exactly once, and you pay for that with a ceiling: 300 messages a second, 3,000 if you batch, tens of thousands in high-throughput mode. Build a real queue starting with its safety net, a dead-letter queue (DLQ). That is a second queue that catches messages which keep failing, so one poisoned message can't loop forever and drown your workers.
# 1. The DLQ first — it's just another queueaws sqs create-queue --queue-name orders-dlq# {# "QueueUrl": "https://sqs.eu-west-1.amazonaws.com/123456789012/orders-dlq"# }# 2. Grab its ARN for the redrive policyaws sqs get-queue-attributes \--queue-url https://sqs.eu-west-1.amazonaws.com/123456789012/orders-dlq \--attribute-names QueueArn# "QueueArn": "arn:aws:sqs:eu-west-1:123456789012:orders-dlq"# 3. Work queue: 20s long polling, 90s visibility, DLQ after 5 failed receivesaws sqs create-queue --queue-name orders --attributes '{"ReceiveMessageWaitTimeSeconds": "20","VisibilityTimeout": "90","RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:eu-west-1:123456789012:orders-dlq\",\"maxReceiveCount\":\"5\"}"}'
Two of those settings carry real weight, and the exam pokes at both. ReceiveMessageWaitTimeSeconds: 20 switches on long polling. Without it, an empty queue answers your poll instantly with nothing, and you get billed for the round trip anyway. With it, SQS holds the line open for up to 20 seconds and answers the moment work shows up. Fewer API calls, smaller bill, quicker pickup. The second setting, VisibilityTimeout, exists because *receiving a message does not delete it*. Borrowing a library book works the same way: the book leaves the shelf, nobody else can take it, and there is a due date. When a consumer receives a message, SQS hides it from every other consumer for the length of the timeout (30 seconds by default, 12 hours at most). The consumer does the work, then explicitly deletes the message using the ReceiptHandle that came back with it. Crash before that delete and the clock runs out, the message pops back into view, and someone else picks it up. That quiet retry is the entire fault-tolerance story, and it is also why standard queues promise *at least once* and never *exactly once*.
QUEUE=https://sqs.eu-west-1.amazonaws.com/123456789012/ordersaws sqs send-message --queue-url $QUEUE \--message-body '{"orderId":"1042","total":149.99}'# {# "MD5OfMessageBody": "8ef4d2a9c1b7e6f3...",# "MessageId": "c3b9e0f1-6f2a-4c8e-9a51-1d2f3e4a5b6c"# }aws sqs receive-message --queue-url $QUEUE \--wait-time-seconds 20 --max-number-of-messages 10# {# "Messages": [{# "MessageId": "c3b9e0f1-6f2a-4c8e-9a51-1d2f3e4a5b6c",# "ReceiptHandle": "AQEBzL5q8XoGm3...",# "Body": "{\"orderId\":\"1042\",\"total\":149.99}"# }]# }# Work done -> delete, or the message comes back in 90 secondsaws sqs delete-message --queue-url $QUEUE \--receipt-handle "AQEBzL5q8XoGm3..."# (no output = success)
maxReceiveCount and perfectly healthy messages start piling into your DLQ while they are still being processed.SNS: one announcement, everyone who cares hears it
A tannoy announcement in a warehouse goes out once and everyone on the floor hears it at the same time. The person holding the microphone has no idea who is listening, and that is exactly the point. Amazon SNS (Simple Notification Service) works that way, which flips the direction of SQS. It is publish/subscribe, and it *pushes*. A publisher sends one message to a topic, which is a named channel, and SNS hands a copy to every subscriber on it: SQS queues, Lambda functions, HTTPS endpoints, email, SMS, mobile push, or Amazon Data Firehose (the service that used to be called Kinesis Data Firehose). Adding a new listener needs zero changes to the publisher. SNS stores nothing for later pickup, which is why the standard pattern is fan-out: the topic drops a copy into several SQS queues, one per consuming service, and each queue absorbs its own backlog. One trap catches everybody once. SNS can only deliver into a queue whose access policy names sns.amazonaws.com and allows sends from that topic's ARN (Amazon Resource Name, the unique ID AWS gives every resource). A missing queue policy is the number one reason a fan-out silently delivers nothing at all. Filter policies cut the noise: a subscription with a filter receives only the messages whose attributes match, so the refunds service isn't woken up by every order that gets placed.
aws sns create-topic --name order-events# { "TopicArn": "arn:aws:sns:eu-west-1:123456789012:order-events" }# Subscribe a queue; raw delivery skips the SNS JSON envelopeaws sns subscribe \--topic-arn arn:aws:sns:eu-west-1:123456789012:order-events \--protocol sqs \--notification-endpoint arn:aws:sqs:eu-west-1:123456789012:refunds \--attributes '{"RawMessageDelivery":"true"}'# { "SubscriptionArn": "arn:aws:sns:eu-west-1:123456789012:order-events:7f3c9a1e-..." }# Only refund events reach this queueaws sns set-subscription-attributes \--subscription-arn arn:aws:sns:eu-west-1:123456789012:order-events:7f3c9a1e-... \--attribute-name FilterPolicy \--attribute-value '{"eventType":["refund"]}'aws sns publish \--topic-arn arn:aws:sns:eu-west-1:123456789012:order-events \--message '{"orderId":"1042"}' \--message-attributes '{"eventType":{"DataType":"String","StringValue":"refund"}}'# { "MessageId": "b5e0a3d2-91c4-4f7e-8a2b-0c1d2e3f4a5b" }
EventBridge: reading the letter, not the envelope
A sorting office that opens the letter and routes it on what is written inside can do things one that only reads the label never could. Amazon EventBridge is that sorting office: a serverless event bus, which is SNS with a rules engine bolted on the front. An event is a JSON document carrying a source, a detail-type, and a detail payload. Rules match on the *content* of that document, the fields inside it rather than a flat list of attributes stuck to the outside, and they send matches to targets: Lambda, Step Functions, SQS, a bus in another AWS account, and twenty-odd other services. Three things separate it from a nicer SNS. First, the default bus already carries the events AWS emits about itself (*an EC2 instance changed state*, *someone signed in to the console*), and SaaS vendors such as Datadog and Stripe can push their own events into a partner event bus you attach to your account. Either way you react to a vendor without writing a poller that hammers their API every minute. Second, the matching is rich: numeric ranges, prefixes, anything-but. That makes 'orders over $100 go to fraud review' a line of routing config instead of an if statement buried in consumer code. Third, archive and replay records events so you can push them back through the bus later, which saves your week when a buggy consumer silently binned seven days of traffic. The price: EventBridge usually adds more delivery latency than SNS, and its PutEvents API is capped by regional quotas, while SNS standard topics take nearly unlimited throughput and deliver in near real time.
aws events put-rule --name big-orders \--event-pattern '{"source": ["com.shop.orders"],"detail-type": ["OrderPlaced"],"detail": {"total": [{"numeric": [">", 100]}]}}'# { "RuleArn": "arn:aws:events:eu-west-1:123456789012:rule/big-orders" }aws events put-targets --rule big-orders \--targets '[{"Id":"fraud-check","Arn":"arn:aws:sqs:eu-west-1:123456789012:fraud-check"}]'# { "FailedEntryCount": 0, "FailedEntries": [] }# NB: as with SNS, delivery only works once the queue's access policy# lets events.amazonaws.com send from this rule's ARN# Emit a custom event and let the rule route itaws events put-events --entries '[{"Source": "com.shop.orders","DetailType": "OrderPlaced","Detail": "{\"orderId\":\"1042\",\"total\":149.99}"}]'# { "FailedEntryCount": 0, "Entries": [{ "EventId": "5f9a3b7c-2e41-..." }] }
Telling them apart in an exam question
Scenario questions turn on a short list of tells. One producer, one consumer, work buffered and done once → SQS. One event, several consumers that each need their own copy → SNS, usually fanning into SQS. Routing on what the event contains, reacting to AWS or SaaS events, crossing account boundaries, or replaying history → EventBridge. Strict ordering *and* no duplicates → the FIFO version of SQS or SNS. And when the question mentions real-time streaming analytics, or several consumers re-reading the same ordered records, the answer is Kinesis Data Streams and none of these three: SQS deletes a message once it is consumed, while Kinesis keeps the stream so every consumer reads at its own cursor. Track the direction of travel too. SQS consumers *pull*. SNS and EventBridge *push*. That pull model is why queue depth, the ApproximateNumberOfMessagesVisible metric, is the standard scaling signal for a fleet of workers, as you saw in the Auto Scaling lesson.
Before it leaves the lab: security, scale, cost
Three levers matter in production. Security: new SQS queues arrive with SSE-SQS (server-side encryption at rest, using a key AWS manages) already switched on, but compliance workloads usually want SSE-KMS, which uses a customer-managed key in AWS Key Management Service so *you* own the key policy and the rotation schedule. Write resource policies that grant the least access that still works: only the topic may SendMessage to the fan-out queues, only the worker role may ReceiveMessage. Add VPC endpoints, private doors into AWS services from your Virtual Private Cloud, so queue traffic never touches the public internet. Scale: memorize 256 KB per message, still the answer the exam wants, though AWS raised the real maximum to 1 MiB in August 2025. The classic workaround for anything bigger is to park the payload in S3 and queue a pointer to it. Remember roughly 120,000 in-flight messages on a standard queue, plus the FIFO throughput ceiling from earlier. Cost: SQS bills per request, around $0.40 per million once you leave the free tier, so long polling saves money as well as latency by wiping out most of your empty ReceiveMessage calls. Custom EventBridge events run about $1 per million. The cheapest architecture tends to be the calmest one: fewer synchronous calls, fewer retry storms pounding a service that is already struggling.
Notice how much of this came back to permissions. A queue policy that is too generous lets anybody inject work into your pipeline, and an unencrypted topic hands over every payload it carries. Every arrow you draw between two decoupled components is also a door somebody can try. Resource policies, customer-managed keys, private endpoints: those are three instances of one discipline, and the next lesson, Security best practices, applies that discipline across your whole AWS account.
So: SQS holds work until a worker is free. SNS copies one event to everybody subscribed. EventBridge reads the event and decides where it goes, including into other accounts and out to SaaS vendors. Pick by the relationship you actually have. One worker draining a backlog is a different problem from ten teams all reacting to "OrderPlaced."
Try this
Spin up a throwaway queue, put one message in, pull it back out, then delete the queue. Do this in a lab account, never production.
Q=$(aws sqs create-queue --queue-name saa-lab-q-$RANDOM --query QueueUrl --output text)echo "Queue=$Q"aws sqs send-message --queue-url "$Q" --message-body '{"orderId":"o-42","action":"bill"}'aws sqs receive-message --queue-url "$Q" --max-number-of-messages 1 --wait-time-seconds 5 \--query 'Messages[0].{Body:Body,Handle:ReceiptHandle}' --output tableaws sqs delete-queue --queue-url "$Q"
Queue=https://sqs.us-east-1.amazonaws.com/111122223333/saa-lab-q-18422---------------------------------------------| ReceiveMessage |+---------------------------+---------------+| Body | Handle |+---------------------------+---------------+| {"orderId":"o-42"...} | AQEB... |+---------------------------+---------------+# delete-queue removes the lab queue
Takeaway
Queues absorb spikes. Topics copy one event to many. EventBridge routes on what the event says. Loose coupling is how a producer survives a consumer that went slow or died overnight.
Next: take one synchronous internal API call from a system you know, redraw it with SQS or EventBridge in the middle, and name the failure you removed by doing it.