Notes · Learn AWS · CHAPTER 7

Messaging & Events

The closest 1:1 cloud mapping in the whole book. Service Bus → SQS, Event Grid → EventBridge, Event Hubs → Kinesis. The vocabulary changes, the patterns don't. Pick the right pipe (queue, pub/sub, event router, or stream) and the architecture mostly draws itself.

Azure messaging is famously a four-service spread: Service Bus (durable enterprise queues + topics), Event Grid (push-based event routing), Event Hubs (high-throughput ingestion), and Storage Queues (the cheap legacy option). AWS has the same four shapes - just renamed. The mappings line up so neatly that this chapter is mostly about learning the new words and the handful of places the defaults are different (visibility timeout, FIFO throughput, batching).

In this chapter
  1. The cheat table
  2. SQS - the workhorse queue
  3. SNS - pub/sub and fan-out
  4. EventBridge - the event router
  5. Kinesis - streaming data
  6. Lambda triggers (event source mappings)
  7. Choosing between them
  8. Try it: list every messaging service
  9. Quick check (quiz)
  10. Gotchas for Azure devs
  11. Project Compass: the intake queue
  12. Recap & next

The cheat table: Azure messaging → AWS messaging

What you want to doAzureAWS
Durable work queueService Bus QueueSQS Standard Queue
Ordered, exactly-once queueService Bus Queue (Sessions + dedup)SQS FIFO Queue (.fifo suffix required)
Topic / pub-sub fan-outService Bus Topic + SubscriptionsSNS Topic + Subscriptions
Reactive event routing (push)Event Grid (custom topics, system topics)EventBridge (default bus, custom buses, partner buses)
High-throughput stream ingestEvent HubsKinesis Data Streams
Stream-to-storage pipelineEvent Hubs Capture → ADLS / BlobKinesis Data Firehose → S3 / Redshift / OpenSearch
Stream SQL / analyticsAzure Stream AnalyticsKinesis Data Analytics (managed Apache Flink)
Workflow orchestrationLogic Apps / Durable FunctionsStep Functions
Cheap, simple queueStorage Queue (the original, 2008)SQS Standard (no cheaper tier exists - SQS is already pennies)
Schema registryEvent Hubs Schema RegistryEventBridge Schema Registry + Glue Schema Registry
Dead-letter destinationDLQ sub-queue on Service BusA separate SQS queue, referenced by redrive policy
Cross-region replicationService Bus Geo-DR pairingNo built-in - use SNS cross-region subscriptions or replicate at app level
The row to internalize: Service Bus FIFO ↔ SQS FIFO. Both add a .fifo-style marker (Azure: RequiresSession=true, AWS: queue name must end in .fifo). Both promise ordering within a group. Both cap throughput in exchange. The semantics line up almost feature-for-feature - including the gotcha that "FIFO" only orders within a session/group, not across the whole queue.
What's in a name? - the messaging glossary
SQS
Simple Queue Service. Launched in public beta in 2004 and went GA in July 2006 - this makes SQS the oldest "service" in AWS by API age, even though S3 (March 2006) was the first GA service to the public. The "Simple" was a deliberate jab at the Enterprise Service Bus systems of the era; SQS has no transactions, no schemas, no priority - just put-and-get.
SNS
Simple Notification Service. Launched 2010. Same "Simple" sibling-naming as SQS. Pub/sub with delivery to email, SMS, HTTP, SQS, Lambda, and mobile push - all from one topic.
EventBridge
Originally launched in 2014 as CloudWatch Events. Renamed to EventBridge in July 2019 when AWS added partner event sources (Zendesk, PagerDuty, Datadog, etc.). The underlying API is still backwards-compatible with the CloudWatch Events one - you'll see both names in old docs.
Kinesis
From the Greek kínēsis meaning "movement" or "motion" - same root as "kinetic". AWS picked the name in 2013 to evoke "data in motion", in contrast to S3 (data at rest). Predates Apache Kafka's enterprise popularity by several years.
DLQ
Dead-Letter Queue. Term borrowed from postal services - "dead letter" was 19th-century mail that couldn't be delivered or returned. In messaging, it's where a message goes after exceeding its retry count, so it can be inspected later without blocking healthy traffic.
FIFO
First-In, First-Out. Ordering guarantee from operating-system queue theory. AWS borrows the term unchanged - an SQS FIFO queue delivers messages in the exact order they were sent within a single MessageGroupId. Different groups can interleave freely; that's how FIFO scales.
"Visibility timeout"
The window during which a polled message is hidden from other consumers. After this window, if the message wasn't deleted, it reappears and another consumer can pick it up. Default: 30 seconds. The single most consequential SQS setting.
Fun fact AWS likes to claim SQS is "the oldest AWS service" because its public beta started in late 2004 - a full 16 months before S3 went public in March 2006. But SQS only hit general availability in July 2006, four months after S3. So depending on how you score it, either SQS (longest API age) or S3 (first GA launch) gets the crown. Either way, both predate EC2 (Aug 2006), and the entire foundation of cloud computing was built on "S-something" services launched in a six-month window in 2006.

SQS - the workhorse queue

ELI5: how SQS works
Imagine a bakery counter ticket dispenser. Customers (producers) take a ticket and drop it into a basket. Bakers (consumers) grab a ticket from the basket and start working on the order. When a baker grabs a ticket, that ticket gets a 30-second "I'm working on this" flag - other bakers can't take it. If the baker finishes and tears up the ticket, it's done. If the baker forgets or drops dead, the flag expires and the ticket goes back in the basket for another baker. That's literally SQS - visibility timeout is the "I'm working on this" flag, and message deletion is tearing up the ticket.

SQS comes in two flavors. Pick the wrong one and you'll spend a chapter swearing at "near-duplicate" messages.

Standard Queue

At-least-once delivery, best-effort ordering, virtually unlimited throughput.

The default. Cheap, fast, and the queue may occasionally duplicate or reorder a message. Your consumer must be idempotent.

Use for: background work, decoupling services, retry buffers.

FIFO Queue

Strict ordering within a MessageGroupId, exactly-once processing.

Name must end in .fifo. Default throughput: 300 transactions/sec without batching, 3,000/sec with batching. "High throughput FIFO" mode lifts this to 70,000+/sec by spreading across groups.

Use for: financial transactions, anything order-sensitive.

DLQ

A second queue that receives messages after N failed receives.

Not a queue type, just a redrive policy pointing one queue at another. Set maxReceiveCount = 5 and after 5 polls without deletion the message moves to the DLQ for human inspection.

Always set one. The first time a message is poisoned, you'll thank yourself.

The consumer poll loop

The SQS consumer loop - and where things go wrong SQS Queue order-intake msg-1 (visible) msg-2 (in-flight, 23s left) msg-3 (visible) msg-4 (visible) VisibilityTimeout=30s Consumer 1. ReceiveMessage 2. Process the work 3. DeleteMessage(handle) 4. loop (or extend visibility if step 2 is slow) DLQ order-intake-dlq maxReceiveCount=5 poll delete (success) no delete → reappears after 5 fails
SQS is pull-based - consumers ask for messages. The visibility timeout is what gives the consumer time to process without competition. Delete-after-success is the contract; forget it and the message comes back forever.

Side-by-side: create a queue

Azure · Service Bus queue
# Create namespace then queue
az servicebus namespace create \
  --name sb-orders \
  --resource-group rg-app \
  --sku Standard

az servicebus queue create \
  --namespace-name sb-orders \
  --resource-group rg-app \
  --name order-intake \
  --max-delivery-count 5 \
  --lock-duration PT30S \
  --enable-dead-lettering-on-message-expiration true

# Dead letter is a sub-entity:
# order-intake/$DeadLetterQueue
AWS · SQS queue
# 1. Create the DLQ first (you need its ARN)
aws sqs create-queue \
  --queue-name order-intake-dlq

DLQ_ARN=$(aws sqs get-queue-attributes \
  --queue-url <dlq-url> \
  --attribute-names QueueArn \
  --query 'Attributes.QueueArn' --output text)

# 2. Create the main queue, point at DLQ
aws sqs create-queue \
  --queue-name order-intake \
  --attributes "{
    \"VisibilityTimeout\": \"30\",
    \"RedrivePolicy\": \"{\\\"deadLetterTargetArn\\\":\\\"$DLQ_ARN\\\",\\\"maxReceiveCount\\\":\\\"5\\\"}\"
  }"

Send and receive

terminal · producer + consumer round trip
# Send a message
aws sqs send-message \
  --queue-url https://sqs.us-east-1.amazonaws.com/123/order-intake \
  --message-body '{"orderId":"o-001","amount":42.50}'

# Poll for messages (long polling - waits up to 20s)
aws sqs receive-message \
  --queue-url https://sqs.us-east-1.amazonaws.com/123/order-intake \
  --wait-time-seconds 20 \
  --max-number-of-messages 10

# Returns Messages[].ReceiptHandle - you NEED this to delete

# Delete after successful processing
aws sqs delete-message \
  --queue-url https://sqs.us-east-1.amazonaws.com/123/order-intake \
  --receipt-handle "AQEB..."
Bug hunt: the queue that won't drain

A new hire writes a consumer loop. They keep seeing the same handful of order IDs in the logs, over and over - every 30-60 seconds. The queue's ApproximateNumberOfMessages metric stays flat at 4. The Lambda the consumer wraps is succeeding (CloudWatch shows no errors). What's wrong with this script?

bad-consumer.sh
#!/usr/bin/env bash
QUEUE_URL="https://sqs.us-east-1.amazonaws.com/123/order-intake"

while true; do
  MSGS=$(aws sqs receive-message \
    --queue-url "$QUEUE_URL" \
    --max-number-of-messages 10)

  # Extract bodies and process them
  echo "$MSGS" | jq -r '.Messages[]?.Body' | while read -r body; do
    process_order "$body"   # succeeds!
  done
done
Click to reveal the bug
No delete-message call.

The script reads messages and processes them - but never tells SQS "I'm done". After the 30-second visibility timeout expires, those messages reappear in the queue, the loop polls them again, and the cycle repeats forever. The "queue won't drain" is misleading - the queue never knew anything got finished.

Fix: capture the ReceiptHandle for each message and call aws sqs delete-message --receipt-handle <handle> right after process_order succeeds. After ~5 visibility-timeout cycles, the message hits maxReceiveCount and lands in the DLQ - so you'll usually notice this bug by watching the DLQ depth climb.

Pro tip: SDKs for Python (boto3) and Go don't auto-delete either. The only thing that auto-deletes is Lambda's SQS event source mapping if the Lambda returns success - that's why Lambda+SQS is so popular.

Real-world incident Visibility timeout shorter than Lambda timeout = exponential duplicate charges $200K refund cycle

A payments startup built a "charge customer" workflow on SQS + Lambda. The SQS queue's visibility timeout was left at the default (30 seconds). The Lambda function timeout was set to 60 seconds because the credit-card processor (Stripe) was sometimes slow on weekends.

On a Saturday, Stripe latency spiked. Lambda invocations started taking 40-55 seconds each. The message would still be in-flight in SQS when the visibility timeout (30s) expired - so SQS made the same message visible again. Lambda's event source mapping promptly polled it and invoked a second instance of the function for the same message. Both instances called Stripe. Both succeeded. The customer got charged twice.

The cascade got worse: with two in-flight Lambdas each taking 50 seconds, both eventually went over their own visibility timeout too, and the message got polled a third and fourth time. By the time monitoring caught it, the worst-affected customer had been charged 8 times for one order.

Lesson: visibility timeout must be at least 2-3x the Lambda function timeout, and Lambda should always check-then-act on a stored idempotency key (e.g. DynamoDB conditional write keyed on orderId). AWS docs say "set visibility timeout greater than your processing time" - in practice, "greater than the function's maximum timeout" is the safe rule. The startup paid 8x refunds plus interchange fees on the duplicate transactions.

SNS - pub/sub and fan-out

ELI5: SNS topics
A magazine has subscribers. The publisher mails one copy to the magazine office (SNS topic). The office prints copies and mails one to each subscriber (subscription). The publisher doesn't know who the subscribers are, doesn't care, and doesn't have to send N copies - they just send one to the office. SNS is the office. Subscribers can be email addresses, phone numbers, SQS queues, Lambda functions, HTTP webhooks, or mobile push tokens. One publisher, many fan-outs.

SNS is push-based pub/sub. You publish to a topic; the topic delivers to every subscription. Compare to Service Bus Topics + Subscriptions - same shape, different vocab.

SNS fan-out: one publish, N deliveries Publisher order-service SNS Topic order-events SQS: warehouse Lambda: analytics HTTP: partner API Email: alerts Mobile push: app 1 publish N fan-out
SNS does not store messages. If a subscription is unreachable, SNS retries per its delivery policy then drops or DLQs. Pair it with SQS to get pub/sub plus durability - the canonical "SNS-to-SQS fan-out" pattern.

The SNS + SQS fan-out pattern

Pure SNS is fire-and-forget; the consumer must be online to receive. The standard trick is to subscribe SQS queues to the topic. Each consumer reads from its own queue, processes at its own pace, and SNS becomes the broadcast bus while SQS becomes the durable buffer.

terminal · subscribe an SQS queue to an SNS topic
# 1. Create the topic
TOPIC_ARN=$(aws sns create-topic --name order-events --query 'TopicArn' --output text)

# 2. Create the consumer queue
QUEUE_URL=$(aws sqs create-queue --queue-name warehouse-orders --query 'QueueUrl' --output text)
QUEUE_ARN=$(aws sqs get-queue-attributes --queue-url "$QUEUE_URL" \
  --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)

# 3. Subscribe the queue to the topic
aws sns subscribe \
  --topic-arn "$TOPIC_ARN" \
  --protocol sqs \
  --notification-endpoint "$QUEUE_ARN"

# 4. CRITICAL: give SNS permission to send to the queue
# (resource-based policy on the queue - chapter 2 territory)
aws sqs set-queue-attributes \
  --queue-url "$QUEUE_URL" \
  --attributes Policy='{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Principal":{"Service":"sns.amazonaws.com"},"Action":"sqs:SendMessage","Resource":"'"$QUEUE_ARN"'","Condition":{"ArnEquals":{"aws:SourceArn":"'"$TOPIC_ARN"'"}}}]}'

Cross-account subscriptions

SNS topics can list principals from other AWS accounts. The topic's access policy grants sns:Subscribe to the other account; the other account's queue policy must allow sns:SendMessage from the topic ARN. The same two-sided handshake we covered in chapter 2 for cross-account access.

Azure equivalent: Service Bus topic subscriptions can use SQL filters on message properties. SNS supports the same idea via message filtering policies on subscriptions - JSON expressions that test message attributes (not body). Filter policies are evaluated before delivery, so unmatched messages cost zero - useful for routing one topic to many specialized consumers.

EventBridge - the event router

ELI5: EventBridge vs SNS
SNS is the magazine office - one in, many out, no questions asked. EventBridge is the magazine office plus a mail-sorting room. You hand it an event; it inspects the event's contents and decides which subscribers should get it based on rules ("if it's a paid order from California, send to the tax service; if it's a refund, send to the refund-handler"). SNS routes by topic name; EventBridge routes by event contents.

EventBridge is AWS's general-purpose event bus. It started life as CloudWatch Events (handling AWS service events like "EC2 instance state changed") and grew into a routing fabric that consumes events from AWS services, partner SaaS apps (Zendesk, PagerDuty, Datadog), and your own custom apps - then routes them to targets based on JSON pattern matching.

Event Bus

A pipe events flow through. Three kinds:

default - receives all AWS service events (EC2, S3, etc.).

custom - your own app events.

partner - SaaS-sourced (created by the partner).

Rule

A pattern matcher attached to a bus. When an event matches, the rule fires one or more targets. Up to 5 targets per rule, 300 rules per bus.

Patterns are JSON: {"source": ["aws.ec2"], "detail-type": ["EC2 Instance State-change Notification"]}

Target

Where the matched event goes. 30+ options: Lambda, SQS, SNS, Step Functions, Kinesis, ECS task run, API destinations (arbitrary HTTPS), another EventBridge bus.

Side-by-side: an AWS console event triggers a Lambda

Azure · Event Grid (S3-like blob created)
az eventgrid event-subscription create \
  --name on-blob-created \
  --source-resource-id "/subscriptions/.../mystg" \
  --endpoint "https://func.azurewebsites.net/api/handle" \
  --endpoint-type webhook \
  --included-event-types "Microsoft.Storage.BlobCreated" \
  --subject-begins-with "/blobServices/default/containers/inbox/"
AWS · EventBridge (S3 object created)
# 1. Define the rule with an event pattern
aws events put-rule \
  --name on-object-created \
  --event-pattern '{
    "source": ["aws.s3"],
    "detail-type": ["Object Created"],
    "detail": {
      "bucket": { "name": ["my-bucket"] },
      "object": { "key": [{ "prefix": "inbox/" }] }
    }
  }'

# 2. Attach the Lambda target
aws events put-targets \
  --rule on-object-created \
  --targets "Id=1,Arn=arn:aws:lambda:us-east-1:123:function:handle"

# 3. Grant EventBridge permission to invoke the Lambda
aws lambda add-permission \
  --function-name handle \
  --statement-id allow-eventbridge \
  --action lambda:InvokeFunction \
  --principal events.amazonaws.com

Content-based routing pattern

The killer feature: rules can inspect any field of the event JSON. This is strict matching - exact string, prefix, suffix, anything-but, numeric range, IP CIDR, or null check. No regex, no SQL.

routing-pattern.json · high-value premium orders only
{
  "source": ["com.compass.orders"],
  "detail-type": ["OrderPlaced"],
  "detail": {
    "customer": {
      "tier": ["premium", "enterprise"]
    },
    "amount": [{ "numeric": [">=", 1000] }],
    "region": [{ "anything-but": ["sanctions-blocked"] }]
  }
}
EventBridge vs SNS in one sentence: use SNS when fan-out is the goal and routing is by topic; use EventBridge when the routing logic itself is the point. EventBridge events also carry richer schema (source, detail-type, account, region, time) that SNS messages don't.

Schema Registry and partner events

EventBridge can infer the schema of events flowing through a bus and generate strongly-typed bindings (Java, Python, TypeScript). You then call aws events put-events with that type and AWS validates. The Schema Registry is the closest AWS gets to "API contracts for events".

Partner event sources are SaaS apps that AWS has integrated. Zendesk pushes ticket-created events directly to your EventBridge bus. PagerDuty, Datadog, Auth0, Shopify - 100+ partners. The partner creates a partner event source in your account, which you "associate" with a partner event bus.

Kinesis - streaming data

ELI5: queues vs streams
A queue is a ticket dispenser - each ticket goes to exactly one baker, and once torn up it's gone forever. A stream is a movie reel - everyone watching can see every frame, you can rewind to any moment in the last 24 hours (or up to 365 days if you pay), and you can have N independent viewers each "checkpointing" where they're up to. Queues are for work distribution; streams are for replay, replay across multiple readers, and time-ordered analytics.

Kinesis is the AWS Event Hubs equivalent - high-throughput, multi-consumer, replay-capable stream storage. It's actually three related services with confusingly similar names.

Kinesis Data Streams

Raw stream storage. You write records to shards; consumers read with their own checkpoint. Retention 1-365 days. Throughput per shard: 1 MB/s in, 2 MB/s out.

Closest Azure analog: Event Hubs (almost exact).

Kinesis Data Firehose

Fully-managed stream-to-storage pipe. Reads from a stream (or accepts direct puts) and writes batches to S3, Redshift, OpenSearch, Splunk, or a generic HTTP endpoint. No shards to manage; no consumer code to write.

Closest Azure analog: Event Hubs Capture.

Kinesis Data Analytics

Managed Apache Flink for stream SQL / Java jobs. Reads from streams, runs windowed aggregations, writes to streams/Firehose/Lambda.

Closest Azure analog: Stream Analytics.

Shards, partition keys, retention

A Kinesis stream is a collection of shards (partitions) Producers Shard 1 · keys hashed 0x00-0x55 Shard 2 · keys hashed 0x55-0xAA Shard 3 · keys hashed 0xAA-0xFF Consumer A (analytics) Consumer B (Firehose) Consumer C (replay) Each shard: 1 MB/s in, 2 MB/s out, 1000 records/sec write, ordered within the shard. Multiple consumers each maintain their own offset (checkpoint).
Partition keys decide which shard a record lands in. Records with the same partition key always land in the same shard and stay ordered. Choose a partition key that spreads load - userId or orderId rather than a hot constant.
AspectSQSKinesis Data Streams
Delivery modelPull, one consumer per messagePull, many consumers each see every record
RetentionUp to 14 days1-365 days (configurable; longer costs more)
ReplayNo - once deleted it's goneYes - any consumer can rewind to an earlier offset
Throughput limitUnlimited (standard); 300 TPS (FIFO default)1 MB/s per shard write, 2 MB/s per shard read
OrderingFIFO queues: ordered per MessageGroupIdAlways ordered within a shard (partition key)
Cost modelPer request ($0.40/M API calls)Per shard-hour + per-record PUT cost

Producer/consumer code shape

terminal · write and read
# Create a stream with 2 shards
aws kinesis create-stream --stream-name clickstream --shard-count 2

# Producer: send a record
aws kinesis put-record \
  --stream-name clickstream \
  --partition-key user-42 \
  --data "$(echo -n '{"event":"click","ts":1716393600}' | base64)"

# Consumer: read from a shard (low-level - real code uses KCL)
SHARD_ITER=$(aws kinesis get-shard-iterator \
  --stream-name clickstream \
  --shard-id shardId-000000000000 \
  --shard-iterator-type TRIM_HORIZON \
  --query 'ShardIterator' --output text)

aws kinesis get-records --shard-iterator "$SHARD_ITER" --limit 100
In production, never call get-records by hand. Use the Kinesis Client Library (KCL) which handles shard assignment, checkpointing (in DynamoDB), and consumer-group coordination. Or skip it entirely - Lambda's event source mapping for Kinesis handles checkpointing for you transparently.

Lambda triggers (event source mappings)

The most common pattern in AWS messaging: "X arrives in service Y, invoke Lambda Z with it". The plumbing that makes this work is an event source mapping - a polling agent that AWS runs on your behalf inside the Lambda service, reading from SQS / Kinesis / DynamoDB Streams / MSK / MQ / DocumentDB and invoking your function with batches.

Event Source Mapping = AWS-managed poller between source and Lambda Source SQS / Kinesis / DynamoDB Streams Event Source Mapping poller (AWS-managed) BatchSize, BatchWindow Lambda Function invoked with batch of N records long-poll invoke success → ESM deletes (SQS) or checkpoints (Kinesis)
The poller is part of Lambda, not your function. It uses long polling, scales out workers, and respects your batch settings. Your function just receives a JSON payload with N records.

Batching: when does the function actually fire?

The poller fires Lambda when any of these triggers:

  1. BatchSize reached - default 10 for SQS, 100 for Kinesis/DynamoDB Streams, configurable up to 10,000.
  2. MaximumBatchingWindowInSeconds elapsed - 0 by default for SQS (fire as soon as anything arrives), up to 300s.
  3. Payload limit hit - 6 MB total invocation payload.

So if you set BatchSize=100, MaxBatchingWindow=5: the function fires when 100 messages are buffered OR after 5 seconds (whichever first). Trade-off: bigger batches = more efficiency but higher per-message latency.

terminal · wire SQS to Lambda
aws lambda create-event-source-mapping \
  --function-name process-orders \
  --event-source-arn arn:aws:sqs:us-east-1:123:order-intake \
  --batch-size 10 \
  --maximum-batching-window-in-seconds 5 \
  --function-response-types ReportBatchItemFailures   # partial-batch responses

# Lambda's execution role MUST have sqs:ReceiveMessage,
# sqs:DeleteMessage, sqs:GetQueueAttributes on the queue.
Partial batch responses (modern best practice): if your Lambda processes 10 SQS messages and 3 fail, the OLD behavior was to throw and have all 10 retried (causing 7 duplicate successes). With --function-response-types ReportBatchItemFailures, the function can return {"batchItemFailures": [{"itemIdentifier": "msg-3"}, ...]} and only those 3 are returned to the queue. This single setting prevents the dreaded "poison message starves the queue" pattern.

Choosing between them

Four services, overlapping pictures on the marketing diagrams. The decision tree below is what most teams actually use.

ServicePick whenAvoid when
SQS One consumer per message; durable work distribution; need DLQ & retries. You need to broadcast to many consumers (use SNS or EventBridge).
SNS Fan-out to a small fixed set of subscribers (email, SMS, SQS, Lambda); no content routing needed. You need stored events, replay, or content-based routing (use EventBridge or Kinesis).
EventBridge Many sources (AWS services, partner SaaS, custom), routing based on event payload, low-medium throughput (~10K events/s default). You need ordered delivery (use SQS FIFO) or millions of records/second (use Kinesis).
Kinesis High-volume ingestion (clickstreams, IoT, logs), multiple independent consumers, replay needed. You need exactly-once or transactional semantics (use SQS FIFO) or low-volume ad-hoc routing (use EventBridge).
Step Functions You're orchestrating a multi-step workflow with retries, branches, parallel fan-out, human approvals. You're moving messages between two services - that's SQS/SNS/EventBridge, not workflow.
Decision tree: which messaging service? One consumer or many? Need ordering? Need replay / history? SQS FIFO SQS Standard Kinesis EventBridge / SNS (routing → EB; raw fan-out → SNS) one many yes no yes no
First branch is the one most worth memorizing - "one consumer or many?" eliminates half the AWS messaging catalog immediately.
Cost trap: short polling on SQS at scale ~$5,000 / month

A team's worker fleet uses the SQS SDK defaults - which means short polling (the receive-message call returns immediately, even if the queue is empty). The receive loop runs as fast as it can, around 100 requests/second per worker. The fleet is 50 workers across 4 services. SQS API calls are billed at $0.40 per million. What does the monthly bill look like, and how do you fix it?

Click to reveal the cost
Maths:

100 req/sec × 86,400 sec/day × 30 days = 259 million calls per worker per month.

259M × $0.40 / 1M = ~$103/month per worker.

50 workers × $103 = ~$5,150/month - on a service that's supposed to be "pennies".

Fix: set --wait-time-seconds 20 (or the SDK's WaitTimeSeconds=20) on every receive-message call. This enables long polling - SQS holds the request open for up to 20 seconds waiting for a message, returning early as soon as one arrives. API call count drops 10-20x while message-arrival latency stays sub-second. Bill drops to ~$300/month for the same workload.

Long polling is universally better than short polling - there is essentially no case where short polling is preferable. AWS docs even warn about this. The default is "short" only because changing it would break existing apps that rely on the immediate return.

Try it: list every messaging service

Lab: enumerate SQS, SNS, EventBridge, Kinesis $0

Goal: get a feel for the AWS messaging surface area in your account. None of these commands cost anything - they're list calls. If your account is new, all four will return empty arrays, which is itself useful (you've now seen the verbs and the responses).

Step 1. List SQS queues. The URL includes your account ID and region - it doubles as an identity check.

terminal
aws sqs list-queues --profile compass
# {
#   "QueueUrls": [
#     "https://sqs.us-east-1.amazonaws.com/123456789012/some-queue"
#   ]
# }

# Bonus: only queues whose name starts with "compass"
aws sqs list-queues --queue-name-prefix compass- --profile compass

Step 2. List SNS topics. Each topic has an ARN; subscriptions hang off that ARN.

terminal
aws sns list-topics --profile compass

# For each topic, show subscriptions:
aws sns list-subscriptions --profile compass

Step 3. List EventBridge buses. You always have one named default - that's the bus all AWS service events flow into.

terminal
aws events list-event-buses --profile compass
# Always at least the "default" bus.

# What rules are on the default bus?
aws events list-rules --event-bus-name default --profile compass

# What targets does a rule have?
aws events list-targets-by-rule --rule <rule-name> --profile compass

Step 4. List Kinesis Data Streams.

terminal
aws kinesis list-streams --profile compass

# Describe a stream's shards and retention:
aws kinesis describe-stream-summary --stream-name <name> --profile compass

What you learned: the four messaging services have four totally different CLI namespaces (sqs, sns, events, kinesis) reflecting their different histories. There's no unified "messaging" command - that fragmentation is itself the lesson. Pick the right service up front; switching later means rewriting the producer and consumer.

Quick check

Test yourself - 5 questions

The numbers in this chapter matter. Sit with each answer before revealing.

1. What is the default visibility timeout for a newly-created SQS queue?

  • 0 seconds (no hiding - any consumer can race)
  • 30 seconds
  • 5 minutes
  • 12 hours (the maximum)
Show answer
Answer: b - 30 seconds. This is the AWS-wide default. It's almost never right for production workloads: web requests usually finish in under a second (so it wastes capacity hiding messages too long), and most async jobs run longer than 30 seconds (so messages reappear before processing finishes - the horror-story scenario). Set it explicitly to something like 2-3x your function's maximum runtime.

2. SQS FIFO queues in their default mode have what throughput cap without batching?

  • 30 messages/sec
  • 300 messages/sec
  • 3,000 messages/sec
  • Unlimited - same as standard queues
Show answer
Answer: b - 300 TPS. FIFO queues cap at 300 transactions/sec (per API operation) without batching. With batching (10 messages per SendMessageBatch call), you effectively get 3,000 messages/sec. AWS also offers High Throughput FIFO which scales to 70,000+ messages/sec by parallelizing across MessageGroupIds - but you have to opt in by setting FifoThroughputLimit=perMessageGroupId and DeduplicationScope=messageGroup. Standard queues, by contrast, are advertised as "nearly unlimited".

3. You have an event source: "an S3 object lands in a specific bucket prefix; if its size is over 100 MB AND its content-type is video/mp4, kick off a Step Functions workflow." Which service routes this best?

  • SNS - subscribe Step Functions to a topic and publish events from S3.
  • SQS - poll for new objects and dispatch.
  • EventBridge - write a rule whose pattern matches on bucket name, prefix, size, and content-type.
  • Kinesis - stream S3 events and have an Analytics job evaluate the condition.
Show answer
Answer: c - EventBridge. This is the textbook EventBridge use case: content-based routing against event payload, with Step Functions as a target. SNS can't filter on numeric ranges or nested fields without extra logic. Kinesis is enormous overkill for an event router. SQS doesn't route - it queues. The rule pattern would look like {"source":["aws.s3"], "detail-type":["Object Created"], "detail":{"bucket":{"name":["my-bucket"]}, "object":{"key":[{"prefix":"videos/"}], "size":[{"numeric":[">",104857600]}]}}}.

4. What is the maximum retention period for a Kinesis Data Stream?

  • 14 days (same as SQS)
  • 30 days
  • 90 days
  • 365 days
Show answer
Answer: d - 365 days. Kinesis defaults to 24 hours, can be set up to 7 days with no extra charge, and up to 365 days with the "long-term retention" pricing tier (extra cost per GB-month after day 7). This is one of Kinesis's killer features versus a queue - you can replay a full year of events into a new consumer. SQS messages, by contrast, max out at 14 days and have no replay at all.

5. You configure a Lambda's SQS event source mapping with BatchSize=100 and MaximumBatchingWindowInSeconds=10. Right now the queue has 27 messages and they're arriving at 1/sec. When will the function fire?

  • Immediately - any message present triggers an invocation.
  • After 100 messages are buffered.
  • After 10 seconds elapse since the first message arrived in this batch.
  • Whichever happens first - 100 messages reached OR 10 seconds elapsed.
Show answer
Answer: d - whichever first. The event source mapping fires the function when any of these triggers: BatchSize reached, batching window elapsed, or 6 MB payload limit hit. With these settings, the function will fire roughly every 10 seconds with batches of ~10 messages (at 1/sec arrival rate) - never reaching the 100-message cap, because the time window will close first. Increase BatchSize without lowering the window and you trade throughput for latency.

Gotchas for Azure devs

1. SQS message body is capped at 256 KB. Azure Service Bus Standard tier is 256 KB too, but Premium tier goes to 100 MB - so Azure-trained reflexes assume "just put the payload in the message". In AWS, if you're going over 256 KB, you use the SQS Extended Client Library: the library writes the payload to S3 and puts a pointer in the SQS message. Common pattern for binary attachments or large JSON.
2. FIFO deduplication window is 5 minutes - and you can't change it. If you send two messages with the same MessageDeduplicationId within 5 minutes, the second is silently dropped. After 5 minutes, the same dedup ID is treated as a new message. This trips up Azure devs who used Service Bus's per-queue dedup-window setting (configurable up to 7 days).
3. EventBridge rule patterns are STRICT JSON, not regex. You can match exact strings, prefixes, suffixes, anything-but, numeric ranges, IP CIDR, and existence-of-field. You cannot match by regex. Many teams hit this when migrating from Azure Event Grid's "advanced filter" syntax (which is also limited but feels different). If your routing needs regex, route to a Lambda and filter there.
4. Kinesis caps at 1 MB/s write per shard. If you exceed it - even briefly - the producer gets ProvisionedThroughputExceededException. There's no auto-scale; you either reshard manually (split shards) or use Kinesis On-Demand mode (more expensive, scales automatically). Event Hubs has a similar Throughput Unit model, but Azure auto-inflates by default in Standard tier - AWS does not unless you opt into on-demand.
5. Long polling vs short polling is a settings choice, not a default. Calling receive-message without --wait-time-seconds uses short polling, which returns immediately even if no messages exist. At scale this can cost thousands of dollars per month in API calls for zero benefit (see the cost-trap above). Always set WaitTimeSeconds=20 in production code. Azure Service Bus doesn't have this distinction - its receive call is always long-poll-style.

Project Compass: the intake queue

The Compass gfn-reports service has grown across the last six chapters: an IAM role (ch2), a VPC (ch3), a Lambda (ch4), an S3 bucket for reports (ch5), and a DynamoDB table for the report index (ch6). Now we wire up the intake side: a Standard SQS queue that the upstream report producers will write to, plus a DLQ for poison messages.

Project Compass · Step 7 of 12 Create gfn-reports-intake queue + DLQ

Why now: chapter 4's Lambda needs an event source. We're using a Standard queue (no ordering required - reports are independent units of work). Visibility timeout is set to 60s, double the Lambda's 30s timeout, so we never re-deliver a message that's still being processed.

This chapter's slice
terminal · create the DLQ first (we need its ARN for the redrive policy)
aws sqs create-queue \
  --queue-name gfn-reports-intake-dlq \
  --attributes '{
    "MessageRetentionPeriod": "1209600"
  }' \
  --tags Project=Compass,ManagedBy=learn-aws \
  --profile compass

# Capture the DLQ ARN for the next step
DLQ_URL=$(aws sqs get-queue-url --queue-name gfn-reports-intake-dlq \
  --profile compass --query 'QueueUrl' --output text)
DLQ_ARN=$(aws sqs get-queue-attributes --queue-url "$DLQ_URL" \
  --attribute-names QueueArn --profile compass \
  --query 'Attributes.QueueArn' --output text)

echo "DLQ ARN: $DLQ_ARN"   # arn:aws:sqs:us-east-1:123:gfn-reports-intake-dlq
terminal · create the main intake queue with redrive policy
aws sqs create-queue \
  --queue-name gfn-reports-intake \
  --attributes '{
    "VisibilityTimeout": "60",
    "MessageRetentionPeriod": "345600",
    "ReceiveMessageWaitTimeSeconds": "20",
    "RedrivePolicy": "{\"deadLetterTargetArn\":\"'"$DLQ_ARN"'\",\"maxReceiveCount\":\"5\"}"
  }' \
  --tags Project=Compass,ManagedBy=learn-aws \
  --profile compass

# Settings explained:
#   VisibilityTimeout=60         - 2x our Lambda timeout (chapter 4 set Lambda to 30s)
#   MessageRetentionPeriod=4d    - default is 4 days; we leave it alone
#   ReceiveMessageWaitTimeSeconds=20 - enables long polling by default (no short-poll cost trap)
#   maxReceiveCount=5            - 5 retries before a message is parked in the DLQ
policies/sqs-access-for-reports-role.json · attach to gfn-reports-role (ch2)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowSendToIntake",
      "Effect": "Allow",
      "Action": ["sqs:SendMessage", "sqs:GetQueueAttributes"],
      "Resource": "arn:aws:sqs:us-east-1:123456789012:gfn-reports-intake"
    },
    {
      "Sid": "AllowReceiveFromIntake",
      "Effect": "Allow",
      "Action": [
        "sqs:ReceiveMessage",
        "sqs:DeleteMessage",
        "sqs:GetQueueAttributes",
        "sqs:ChangeMessageVisibility"
      ],
      "Resource": "arn:aws:sqs:us-east-1:123456789012:gfn-reports-intake"
    }
  ]
}
terminal · attach the policy to gfn-reports-role
aws iam put-role-policy \
  --role-name gfn-reports-role \
  --policy-name gfn-reports-intake-access \
  --policy-document file://policies/sqs-access-for-reports-role.json \
  --profile compass

# Verify both queues are wired correctly
aws sqs list-queues --queue-name-prefix gfn-reports- --profile compass
aws sqs get-queue-attributes \
  --queue-url "$(aws sqs get-queue-url --queue-name gfn-reports-intake --profile compass --query QueueUrl --output text)" \
  --attribute-names All \
  --profile compass | jq '.Attributes | {VisibilityTimeout, RedrivePolicy, ReceiveMessageWaitTimeSeconds}'

We now have an intake queue, a DLQ, long polling enabled by default at the queue level (no caller has to remember to ask), and the gfn-reports-role from chapter 2 has both send and receive permissions. In chapter 8 we'll add KMS encryption to the queue; in chapter 11 we'll wire it to API Gateway as the producer.

Progress
ch1 · profile ch2 · IAM role ch3 · VPC ch4 · Lambda ch5 · S3 ch6 · DynamoDB ch7 · SQS ch8 · KMS ch9 · alarms ch10 · EKS ch11 · API GW ch12 · Terraform
Why Standard and not FIFO? Report intake is naturally idempotent (we'll key DynamoDB writes by reportId, so a duplicate delivery overwrites with the same data) and per-report ordering doesn't matter. FIFO would cap us at 3,000 msg/sec batched and add the 5-minute dedup window quirk for no benefit. Pick FIFO only when ordering or exactly-once is a hard requirement.

Recap & next

What stuck?

The mental model in one sentence

Azure groups messaging into "Service Bus for enterprise" + "Event Grid/Hubs for cloud-native". AWS groups it into "what shape is the data?" - one consumer (SQS), many subscribers (SNS), conditional routing (EventBridge), or replayable stream (Kinesis). Once you can answer "which shape?" in 5 seconds, the rest is just CLI vocab.

Common pitfalls so far

TrapFix
SQS consumer never deletes messagesAlways call delete-message after success. Watch DLQ depth to catch this fast.
Visibility timeout shorter than processing timeBump to 2-3x your function's maximum runtime. Or call change-message-visibility mid-processing.
Short polling burning API callsSet ReceiveMessageWaitTimeSeconds=20 at the queue level (works for all callers).
Wrong service: using SNS for routingIf you're inspecting the message body to decide where it goes, you wanted EventBridge.
Kinesis hot shardChoose a partition key that spreads load. Reshard if a single key dominates.
NEXT CHAPTER
8. Security & Secrets
KMS, Secrets Manager, Parameter Store, and how to keep your SQS queue's messages encrypted at rest without setting fire to the bill.