Messaging · 06

AWS Messaging

SQS, SNS, EventBridge, Kinesis and MSK solve different problems that look alike. This page explains each from first principles, with the queue settings that cause real incidents (visibility timeout, polling, DLQs, FIFO groups), the fan-out and routing patterns, the stream services, how Lambda consumes all of them, how to secure, monitor and pay for them, and a decision guide for choosing.

13
Chapters
5
AWS services
1 MiB
SQS message max
SDK v2
Java examples
01
Chapter One - The Landscape

Which AWS Service Is Which

AWS does not have one messaging service, it has several, and they correspond closely to the ideas from page 01 and the brokers in pages 02 and 05. Amazon SQS is a managed queue, Amazon SNS is managed publish/subscribe, Amazon EventBridge is a serverless event bus with content-based routing, Amazon Kinesis Data Streams is a managed log, and Amazon MSK is managed Apache Kafka. They are usually combined: SNS in front of several SQS queues, EventBridge rules that target a Kinesis stream, Lambda reading from any of them.

Four shapes of AWS messaging
SQS queue Consumers PULL One consumer gets each message Deleted after ack SNS pub/sub topic Service PUSHES A copy to every subscription No storage to replay EventBridge event bus Service PUSHES Rules match event content, up to 5 targets per rule Kinesis / MSK log / stream Consumers PULL Records stay for the retention period Replay by position work queue, buffer, retry notify many, fan-out route events by content ordered, replayable high-volume streams
SQSSNSEventBridgeKinesis Data StreamsMSK
ModelQueueTopic (pub/sub)Event bus, rulesStream of shardsKafka topics and partitions
Delivery to consumerConsumer pollsService pushesService pushesConsumer polls (or push with enhanced fan-out)Consumer polls
Stores messages60 s to 14 days (default 4 days)No (retries only)No (optional archive)24 h default, up to 365 daysKafka topic retention
ReplayNo (message is deleted)No (FIFO topics can archive and replay)Archive and replayYes, from a positionYes, offsets
OrderingFIFO queues, per message groupFIFO topics, per message groupNo ordering guarantee on the classic busPer shard (per partition key)Per partition
Competing consumersYes, built inn/an/aOne reader per shard per applicationConsumer groups
You manageNothingNothingNothingShard count (provisioned mode)Brokers, partitions (not on Serverless)
Key insight: A queue is a to-do tray where each slip is taken by one worker. A topic is a loudspeaker that tells everyone in the room. An event bus is a mail room that reads the address on each envelope and forwards it. A stream is a ledger that readers consume at their own pace and can reread. Most real systems need a tray behind the loudspeaker, which is why SNS to SQS is the most common pattern on AWS.
⚠ Treating SNS or EventBridge as storage
  • SNS and the EventBridge bus deliver and forget. If a subscriber is down for longer than the retry policy allows, the event is dropped unless you attached a dead-letter queue. When a consumer must survive downtime, put an SQS queue (or a stream) between the producer and the consumer.
How this page is organised
  • Chapters 02 to 04 go deep on SQS because its settings cause most production incidents. Chapters 05 to 07 cover SNS, EventBridge, Kinesis and MSK. Chapter 08 is the decision guide, 09 the Lambda glue, 10 operations, 11 Java code and common mistakes.
02
Chapter Two - SQS Core

Queue Types, Limits, Visibility, Polling and Batching

Amazon SQS stores messages redundantly and lets any number of producers and consumers use a queue without you running a broker. The two things to understand first are that delivery is at-least-once and that a received message is only hidden, not removed.

Standard vs FIFO
Standard queueFIFO queue
ThroughputVery high, nearly unlimited API calls per second per action300 transactions per second per API action per partition; up to 3,000 messages per second with batching (300 calls x 10); higher with high throughput mode
OrderingBest effort, can arrive out of orderStrict order within a message group
DuplicatesAt-least-once: occasional duplicatesDeduplication within a 5-minute window on send; processing is exactly-once only if you delete before the visibility timeout expires
NameAnyMust end in .fifo
Message group IDOptional (enables fair queues)Required on every message
Use forWork queues, buffering, fan-out targets where order does not matterPer-entity ordering, no tolerated duplicates (payments, inventory per SKU)

The FIFO figures are from the SQS quotas page. High throughput mode raises the limits by Region (the page lists, for example, up to 70,000 transactions per second in US East (N. Virginia), US West (Oregon) and Europe (Ireland), and lower numbers elsewhere). Look up your Region before sizing a FIFO design.

Life of an SQS message: visible, in flight, deleted (or back again)
Visible in the queue In flight hidden for the visibility timeout Deleted ReceiveMessage DeleteMessage visibility timeout expires without a delete: visible again Dead-letter queue after maxReceiveCount receives A received message is not removed, it is hidden. Only DeleteMessage removes it. Timer: delay (0 s to 15 min) hides a message BEFORE the first receive; visibility timeout hides it AFTER each receive.
Limits you will meet (SQS quotas page, checked 2026-10-31)
ItemValueNote
Message size1 byte to 1,048,576 bytes (1 MiB)Larger payloads: Extended Client Library (up to 2 GB via S3)
RetentionDefault 4 days; 60 seconds to 14 daysPer queue attribute MessageRetentionPeriod
Visibility timeoutDefault 30 s; 0 s to 12 hours12 hours counted from first receive, extensions do not reset it
Delay (message timer / delay queue)0 s default; up to 15 minutesHides a message from consumers after it is sent
Batch sizeUp to 10 messages per batch callTotal batch payload also capped at 1 MiB
Long polling waitUp to 20 secondsWaitTimeSeconds
Message attributesUp to 10 per message
In-flight messagesAbout 120,000 standard; 120,000 FIFOReceived and not yet deleted. See chapter 04 for FIFO groups
Message group ID length128 charactersAlphanumeric and punctuation
Backlog (messages waiting)Unlimited
Visibility timeout and ChangeMessageVisibility

When a consumer receives a message, SQS starts the visibility timeout (30 seconds unless you change it). Other consumers cannot see the message until it ends. If the consumer calls DeleteMessage first, the message is gone. If not (crash, exception, slow processing), the message becomes visible and is delivered again.

ChangeMessageVisibility sets a new timeout for one received message, measured from the moment of the call, so a long job can send a heartbeat that keeps extending it. Setting it to 0 releases the message immediately (a polite way to give up). The ceiling is 12 hours from the first receive. For work that needs longer, split it or use AWS Step Functions.

Visibility timeout (30 s) is shorter than processing (45 s)
▶
The message reappears while the first consumer is still working. A second consumer starts the same job: duplicate work
Both consumers finish and call DeleteMessage
▶
Every ReceiveMessage returns a new receipt handle, and a delete should use the handle from the most recent receive. The first consumer holds an older handle, so its delete may not remove the message
Receive count climbs on every redelivery
▶
After maxReceiveCount the message goes to the DLQ even though the job works, just slowly
Fix
▶
Set the timeout to the realistic worst-case processing time, or heartbeat with ChangeMessageVisibility. With Lambda, use at least six times the function timeout (chapter 09)
Short vs long polling
Short polling (WaitTimeSeconds = 0, the default)Long polling (WaitTimeSeconds 1 to 20)
Servers queriedA weighted random subsetAll servers
ReturnsImmediately, possibly empty even if messages existAs soon as a message is available, empty only after the wait expires
Empty responsesFrequent on a quiet queue (each is a billed request)Rare
Latency for a new messageNext pollImmediate

Use long polling everywhere. Set the queue attribute ReceiveMessageWaitTimeSeconds to 20 so that even a careless consumer long-polls, and watch the NumberOfEmptyReceives metric to find consumers that poll wastefully.

Batch APIs, delay and large messages
  • SendMessageBatch, DeleteMessageBatch and ChangeMessageVisibilityBatch handle up to 10 entries per call. They cut the number of requests (and the request-based bill) by up to ten.
  • A batch call can succeed overall while some entries fail. Always check the Failed list in the response and retry only those entries.
  • You cannot grant permissions to the batch actions separately: permission for SendMessage, DeleteMessage or ChangeMessageVisibility covers the batch version.
  • A message timer (DelaySeconds on one message) or a delay queue (queue-level default) hides new messages for up to 15 minutes. It is not a scheduler for later than that: use EventBridge Scheduler (chapter 06).
  • Messages over 1 MiB: the SQS Extended Client Library for Java (and for Python) writes the payload to Amazon S3 and sends a small message holding the S3 reference, up to 2 GB. It works only with synchronous clients, and the consumer must also use the library.
⚠ Short polling "because it is the default"
  • Short polling returns empty responses on a quiet queue, and every empty response is a request you pay for. It also queries only a sample of servers, so a message can be missed on one call and appear on the next. Long polling at 20 seconds is the right default for nearly every consumer.
03
Chapter Three - Failure Handling

Dead-Letter Queues and Redrive

A poison message (one that always fails) would otherwise be received, fail, reappear, and repeat forever, burning compute and hiding behind healthy traffic. A dead-letter queue (DLQ) is an ordinary SQS queue that the source queue moves a message to once it has been received too many times. It turns an endless retry loop into a visible, inspectable pile.

Dead-letter queue and redrive
Producer Source queue maxReceiveCount = 5 receive Consumer received 5 times, never deleted: moved StartMessageMoveTask (redrive to source) Dead-letter queue same type, account, Region Alarm on DLQ MessagesVisible > 0 Standard queue: the original enqueue timestamp is kept, so the DLQ retention clock keeps running. FIFO queue: the timestamp resets when the message moves. A redrive resets it for every message either way.
Configuring it
JSON - the redrive policy on the SOURCE queue
{ "deadLetterTargetArn": "arn:aws:sqs:eu-west-1:123456789012:orders-dlq", "maxReceiveCount": 5 }
  • maxReceiveCount is how many times a consumer may receive the message before SQS moves it. A value of 1 sends a message to the DLQ after one failed attempt, so one transient error is fatal. The Lambda guide recommends at least 5 for queues read by Lambda.
  • The DLQ must be in the same account and Region as the source queue, and the queue type must match (FIFO source needs a FIFO DLQ, standard needs standard). Create the DLQ first.
  • A redrive allow policy on the DLQ controls which source queues may use it: allow all (the default), byQueue with up to 10 source queue ARNs, or denyAll.
  • Retention: on a standard queue the message keeps its original enqueue timestamp, so a message that spent 3 days in a queue with 4-day retention has 1 day left in the DLQ. Set the DLQ retention longer than the source (up to 14 days) or poisoned messages vanish before you look.
  • Messages moved to a DLQ keep their encryption state; an unencrypted source stays unencrypted in an encrypted DLQ.
Shell - create the DLQ, attach the policy (output omitted)
$ aws sqs create-queue --queue-name orders-dlq --attributes MessageRetentionPeriod=1209600 $ aws sqs set-queue-attributes --queue-url "$ORDERS_URL" \ --attributes '{"RedrivePolicy":"{\"deadLetterTargetArn\":\"arn:aws:sqs:eu-west-1:123456789012:orders-dlq\",\"maxReceiveCount\":\"5\"}"}'

The policy is a JSON document stored as a string inside the attribute map, which is why the quotes are escaped twice. Terraform and CloudFormation hide this, the CLI does not.

Redriving messages back to the source

After you fix the bug, move the dead letters back. SQS has a managed task for it, which replaced the old copy-and-delete scripts. The three API actions are StartMessageMoveTask, ListMessageMoveTasks and CancelMessageMoveTask.

Shell - redrive the DLQ to its source queue
$ aws sqs start-message-move-task --source-arn arn:aws:sqs:eu-west-1:123456789012:orders-dlq $ aws sqs list-message-move-tasks --source-arn arn:aws:sqs:eu-west-1:123456789012:orders-dlq
  • By default messages go back to the queue they came from. You may name another destination of the same type, and set a maximum velocity (messages per second) so the redrive does not swamp the consumer.
  • A redrive task runs at most 36 hours; an account can have up to 100 active tasks.
  • Redriven messages are new messages: new message ID, new enqueue time, fresh retention. Redriven messages interleave with live traffic in the destination.
  • IAM: the caller needs sqs:StartMessageMoveTask, sqs:ReceiveMessage, sqs:DeleteMessage and sqs:GetQueueAttributes on the DLQ, and sqs:SendMessage on the destination. If a queue policy restricts access to a VPC endpoint, add an aws:CalledViaLast exception so the redrive service can act.
  • Moving between FIFO queues replaces the original deduplication ID with the original message ID, so the DLQ does not drop two independent messages that happened to share a deduplication ID.
A consumer throws on a message with bad data
▶
The message becomes visible again after the visibility timeout and is retried, each time costing a receive
The receive count reaches maxReceiveCount
▶
SQS moves it to the DLQ. Healthy messages behind it are unaffected (standard queue)
Nobody watches the DLQ
▶
The message sits there until retention deletes it. The failure is silent and the data is lost. Alarm on ApproximateNumberOfMessagesVisible greater than 0 for the DLQ
You fix the consumer and start a redrive task
▶
Messages return to the source, in the order they were dead-lettered, and are processed normally
⚠ Using a DLQ on a FIFO queue without thinking about order
  • AWS documents that a DLQ on a FIFO queue can break the exact order of messages. The message that fails is moved aside and the next message in its group proceeds, so the group is processed with a gap. For ordered workflows where a gap is worse than a stall, do not use a DLQ, or stop the consumer and fix the data.
A quirk worth knowing

For a standard queue with maxReceiveCount above 3, a message received three or more times without being deleted is moved to the back of the queue, and ApproximateAgeOfOldestMessage then reports the age of the next message instead. A stuck poison message therefore does not make that metric spike. Rely on the DLQ metric and NumberOfMessagesReceived versus NumberOfMessagesDeleted, not on age alone.

04
Chapter Four - Ordering

FIFO Queues: Message Groups, Deduplication and Fair Queues

A FIFO queue gives you ordering per message group, not per queue. Every message must carry a MessageGroupId. Messages in the same group are delivered in the order they were sent, and while one message of a group is in flight, no later message of that group is handed out. Different groups proceed in parallel. So the group ID is the unit of ordering and the unit of parallelism: a design decision, not a detail.

FIFO message groups: one lane per group, one message at a time per lane
group A A1 A2 A3 A4 consumer 1 holds A1 (in flight) group B B1 B2 B3 B4 consumer 2 holds B1 (in flight) group C C1 C2 C3 C4 consumer 3 holds C1 (in flight) A2 stays hidden until A1 is deleted or its visibility timeout expires. Different groups are delivered in parallel; order across groups is not defined. Single group ID for everything = one lane = effectively one consumer at a time.
How the pieces behave
  • Receiving: one ReceiveMessage can return up to 10 messages from the same group, in order. You cannot ask for a particular group. You cannot get the next message of a group until the received ones are deleted or become visible again.
  • A failing message blocks its group. The group waits until the message is deleted or expires (or is moved to a DLQ, see chapter 03). Other groups keep flowing.
  • Producers: if several producers share a group ID, SQS orders their messages by arrival. For strict per-producer order, give each producer its own group ID.
  • Deduplication: a message with the same MessageDeduplicationId as one already accepted within a 5-minute window is acknowledged but not delivered again, even if the first one was already received and deleted. Provide the ID yourself, or enable content-based deduplication and let SQS hash the body.
  • Producer retries are safe. If SendMessage times out, resend with the same deduplication ID. For ReceiveMessage, a retry can reuse the same receive request attempt ID.
Choice of group IDOrdering you getParallelismTypical use
orderId, customerId, accountIdPer entityAs many lanes as active entitiesThe normal choice
A few fixed values (region, shard number)Per bucketCapped at that countBounded, even load
One constantTotal orderOne message at a timeRare: a true global sequence
A random UUID per messageNone (FIFO is wasted)MaximumUse a standard queue instead
Throughput of FIFO

FIFO limits are counted in API requests per partition, not messages. Without high throughput mode each partition handles 300 transactions per second per action, or 3,000 messages per second if every call is a batch of 10. High throughput mode raises that (by Region), and SQS spreads message groups over partitions, so many distinct group IDs let it scale. The in-flight limit for FIFO is 120,000 messages. The CloudWatch metric ApproximateNumberOfGroupsWithInflightMessages shows how many lanes are actually busy: a big backlog with a low value means too few groups or too few consumers.

Java - sending to a FIFO queue (compiled against AWS SDK for Java v2)
import software.amazon.awssdk.services.sqs.SqsClient; public class FifoSender { public static void send(SqsClient sqs, String fifoQueueUrl, String orderId, String json) { sqs.sendMessage(r -> r .queueUrl(fifoQueueUrl) // the queue name ends in .fifo .messageBody(json) .messageGroupId(orderId) // required: ordering is kept per group .messageDeduplicationId(orderId + "-created")); // optional if content-based dedup is on } }
Fair queues for multi-tenant standard queues

A different problem: many tenants share one standard queue and one tenant floods it, so everyone else's messages wait behind the flood (the noisy neighbour problem, measured as dwell time). Amazon SQS fair queues address this without any queue setting: on a standard queue, producers set MessageGroupId to a tenant identifier. SQS watches how many in-flight messages each tenant has, labels a tenant with a disproportionate share as noisy, and prefers messages from the other tenants when it hands messages out.

  • It does not rate-limit the noisy tenant: if consumers have spare capacity and nothing else is waiting, the noisy tenant is served at full speed.
  • Throughput stays as high as a standard queue and there is no limit on the number of tenants. Ordering is not guaranteed (this is a standard queue).
  • The MessageGroupId here means "tenant", not "ordered lane". AWS recommends setting it on every message of a fair queue.
  • Metrics include ApproximateNumberOfNoisyGroups and the ...InQuietGroups variants (for example ApproximateAgeOfOldestMessageInQuietGroups), which exclude the noisy tenants so you can alarm on what other tenants experience.
⚠ One message group for the whole queue
  • With a single group ID a FIFO queue behaves like a one-lane road: only one message is in flight at a time, so adding consumers changes nothing and the backlog grows. Symptom: many idle consumers, a growing ApproximateNumberOfMessagesVisible, and ApproximateNumberOfGroupsWithInflightMessages stuck at 1. Choose a group ID that matches the entity whose events must be ordered.
05
Chapter Five - SNS

Topics, Subscriptions, Filtering and the SNS-to-SQS Fan-Out

Amazon SNS is push-based publish/subscribe. A publisher sends one message to a topic; SNS copies it to every subscription and delivers it to that subscription's endpoint. Subscribers can be Amazon SQS queues, AWS Lambda functions, HTTP/S endpoints, Amazon Data Firehose delivery streams, and (outside this page's focus) email, SMS and mobile push. SNS does not store messages for later reading, so durability on the consumer side comes from the endpoint, which is why the standard pattern is a queue per consumer.

SNS fan-out: one publish, a copy per subscription, filtered
Order service Publish SNS topic order-events SQS billing-orders filter: eventType = OrderPlaced SQS shipping-orders filter: OrderPlaced, OrderPaid SQS audit-all no filter policy: gets everything Lambda notify filter: total >= 100 (body scope) HTTPS partner webhook custom retry policy allowed Each subscription has its own filter policy, own retry, own DLQ. A slow or broken subscriber never delays the others. Put an SQS queue behind each important consumer so messages survive its downtime.
Limits (SNS quotas, checked 2026-10-31)
ItemValue
Message sizeDefault 262,144 bytes (256 KiB); raise up to 1,048,576 bytes (1 MiB) with the topic attribute MaximumMessageSize; larger via the SNS Extended Client Libraries (up to 2 GB)
Batch publishPublishBatch: up to 10 entries per request
Topics per account100,000 standard; 1,000 FIFO
Subscriptions per topic12,500,000 standard; 100 FIFO
Filter policies200 per topic; 10,000 per account
Publish ratePer-account, per-Region (for example 30,000 messages per second in N. Virginia); see the quotas page for yours
Message filtering

A filter policy on a subscription lists attribute names and the values it accepts; SNS delivers only matching messages. By default the policy scope is MessageAttributes; set the subscription attribute FilterPolicyScope to MessageBody to match on fields inside a JSON body. Operators include exact match, prefix, suffix, anything-but, numeric comparisons and ranges, exists, IP address match and case-insensitive equality. A property you do not list in the policy is ignored.

JSON - a FilterPolicy for message attributes
{ "eventType": ["OrderPlaced", "OrderPaid"], "region": [{"prefix": "eu-"}], "amount": [{"numeric": [">=", 100]}] }
  • Filtering is evaluated by SNS before delivery, so unmatched messages cost the subscriber nothing. Different attribute names are AND-ed, several values for one name are OR-ed.
  • Changes to a filter policy can take up to 15 minutes to take full effect (eventual consistency). Do not assume a deploy is instant.
  • On FIFO topics, filtering makes delivery at-most-once for the filtered-out copies, by definition.
SNS to SQS: the permissions and the envelope

Subscribing a queue needs two things: a subscription (protocol sqs, endpoint the queue ARN) and a queue policy that lets the SNS service principal send to that queue. Scope the policy with aws:SourceArn (and optionally aws:SourceAccount) so only your topic can write to the queue.

By default SNS wraps the message in a JSON envelope (fields such as Type, MessageId, TopicArn, Message, Timestamp, Signature, UnsubscribeURL) and your consumer must unwrap Message. Setting the subscription attribute **RawMessageDelivery to true** strips the SNS metadata and delivers the original body as the SQS message body. With raw delivery on SQS, message attributes are carried across as SQS message attributes; for HTTP/S and Firehose endpoints, attributes are not sent.

Java - topic, queue, queue policy and a raw, filtered subscription (compiled against AWS SDK for Java v2)
import software.amazon.awssdk.services.sns.SnsClient; import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.QueueAttributeName; import java.util.Map; public class FanOutSetup { public static void main(String[] args) { SnsClient sns = SnsClient.create(); SqsClient sqs = SqsClient.create(); String topicArn = sns.createTopic(r -> r.name("order-events")).topicArn(); String queueUrl = sqs.createQueue(r -> r.queueName("billing-orders")).queueUrl(); String queueArn = sqs.getQueueAttributes(r -> r .queueUrl(queueUrl) .attributeNames(QueueAttributeName.QUEUE_ARN)).attributes().get(QueueAttributeName.QUEUE_ARN); // Queue policy: let this one topic (and only this topic) send to this queue. String policy = """ {"Version":"2012-10-17","Statement":[{ "Effect":"Allow", "Principal":{"Service":"sns.amazonaws.com"}, "Action":"sqs:SendMessage", "Resource":"%s", "Condition":{"ArnEquals":{"aws:SourceArn":"%s"}}}]} """.formatted(queueArn, topicArn); sqs.setQueueAttributes(r -> r.queueUrl(queueUrl).attributes(Map.of(QueueAttributeName.POLICY, policy))); // Subscribe with raw delivery (no SNS JSON envelope) and a filter on a message attribute. sns.subscribe(r -> r .topicArn(topicArn) .protocol("sqs") .endpoint(queueArn) .attributes(Map.of( "RawMessageDelivery", "true", "FilterPolicy", "{\"eventType\":[\"OrderPlaced\"]}"))); } }

The same wiring in Terraform or CloudFormation is the same three resources: a queue, a queue policy, a subscription. Keep them in one stack so the policy always names the right topic ARN.

Delivery retries and subscription DLQs
Endpoint typeRetry schedule (default policy)Total
SQS, Lambda, Firehose (AWS managed)3 immediate; 2 at 1 s; 10 with exponential backoff 1 to 20 s; then 100,000 at 20 s100,015 attempts over 23 days
HTTP/S, SMTP, SMS, mobile push (customer managed)2 at 10 s; 10 with backoff 10 to 600 s; then 38 at 600 s50 attempts over about 6 hours
  • Only the HTTP/S policy can be customised (the total retry time cannot exceed 3,600 seconds). Retries use jitter.
  • Retries cover server-side errors only. A client-side error (the Lambda function was deleted, the queue policy no longer allows SNS) is not retried.
  • When the policy is exhausted, or on a client error, the message is discarded unless the subscription has a dead-letter queue: an SQS queue in the same account and Region (FIFO queue for a FIFO topic subscription). An encrypted DLQ needs a customer managed key whose key policy lets the SNS service principal use it.
  • Failed deliveries do not change the subscription's state.
FIFO topics

A FIFO topic keeps order per message group and deduplicates by deduplication ID within five minutes, like a FIFO queue. It carries the group ID to subscribed SQS FIFO queues (the group ID is not passed to standard queues). The per-topic default is 3,000 messages per second or 20 MB per second when FifoThroughputScope is Topic, with at most 300 messages per second inside one message group. A FIFO topic allows far fewer subscriptions than a standard one (100 per topic). Use it with SQS FIFO queues for ordered fan-out, such as every consumer seeing price updates for one product in order.

⚠ Skipping RawMessageDelivery and forgetting the envelope
  • A consumer written against the producer's JSON suddenly fails because the body is now an SNS envelope with the real payload as an escaped string in Message. Either unwrap Message in the consumer or enable RawMessageDelivery on the subscription, and keep the choice documented. Mixing both styles in one queue is a classic source of parse errors.
06
Chapter Six - EventBridge

Event Buses, Rules, Targets, Scheduler and Pipes

Amazon EventBridge routes events (JSON documents describing something that happened) from sources to targets according to rules that match on event content. Where SNS fans a message to everyone subscribed, EventBridge looks inside the event and decides, per rule, whether to forward it. AWS services publish their own events (an EC2 state change, an S3 object created) to the account's default bus automatically, which is why EventBridge is also the glue for operational automation.

EventBridge: sources, an event bus with rules, targets
AWS services (EC2, S3, ...) Your applications (PutEvents) SaaS partners Event bus Rule 1: source = app.orders detail-type = OrderPlaced Rule 2: source = aws.s3 detail-type = Object Created Rule 3: source = app.orders detail.total > 100 SQS queue (input transformer) Lambda function Step Functions or API destination Archive (pattern, retention) replay to the same bus later At most 5 targets per rule. Failed deliveries retry for 24 h, then a DLQ if you set one.
Two kinds of custom bus now exist

The EventBridge guide now describes a Custom Event Bus (subscribers instead of rules, events retained for 1 to 365 days, ordered delivery per event group, deduplication, cross-account sharing through AWS RAM, PutEvents and PutRawEvents) next to Custom Event Bus - Classic, the rules-and-targets bus most existing accounts use. AWS says to start new applications with the new bus, and both remain available. This chapter teaches the rule and target model because it is what you will find in existing systems, and the matching, transformation, retry and DLQ ideas carry over. Check the EventBridge user guide for which model your Region and account offer.

Events, rules and patterns

An event has the envelope fields version, id, detail-type, source, account, time, region, resources and a free-form detail. An event pattern has the same shape as the event and matches if every field you list matches. Each value in a pattern is an array (OR), different fields are AND-ed.

JSON - an event pattern
{ "source": ["app.orders"], "detail-type": ["OrderPlaced"], "detail": { "total": [{"numeric": [">", 100]}], "country": [{"prefix": "DE"}] } }
  • A rule belongs to one bus. One event can match several rules; each rule has up to 5 targets.
  • A pattern is limited to 2,048 characters. Rules using wildcards are limited to 30 per bus. The default is up to 300 rules per bus in most Regions.
  • Test patterns in the EventBridge Sandbox before attaching them to a rule.
Targets, input transformers, retries and DLQs
  • Targets include Lambda, SQS, SNS, Kinesis, Step Functions, another event bus, and API destinations (HTTPS endpoints with managed authentication and rate limiting).
  • An input transformer reshapes the event before the target sees it: you define up to 100 variables with JSON path ($.detail.orderId), then write an input template that uses <orderId>. Reserved variables include aws.events.rule-name and aws.events.event.ingestion-time. Use it to send a target only what it needs.
  • Retries: by default EventBridge retries a failed delivery for 24 hours, up to 185 times, with exponential backoff and jitter. Errors that cannot succeed (missing permission, target deleted) are not retried. Both limits are configurable per target.
  • DLQ: attach an SQS queue as a dead-letter queue to a target, otherwise an event whose retries are exhausted is dropped.
  • The classic bus has no ordering guarantee and at-least-once delivery, so targets must be idempotent (chapter 10).
Archive and replay, schema registry
  • An archive stores events from one source bus, optionally filtered by a pattern, for a number of days you choose (indefinitely by default). Replay re-sends archived events to the bus that received them, which is how you recover from a bug or test a new rule against real traffic. Allow about 10 minutes after the events arrived before replaying, and note that EventBridge adds a managed rule so replayed events are not archived again.
  • The schema registry stores schemas (AWS event schemas, schemas discovered from live traffic, and custom ones) so developers can generate code bindings instead of hand-writing event classes.
EventBridge Scheduler and Pipes
  • EventBridge Scheduler is a separate serverless scheduler: cron and rate expressions, or one-time schedules, with flexible time windows, retries and a maximum retention for failed triggers. Delivery is at-least-once. Templated targets exist for SQS, SNS, Lambda and EventBridge, and universal targets reach more than 270 services and over 6,000 API operations. Use it for "run this once at 09:00 next Tuesday", something SQS delay (15 minutes at most) cannot do.
  • EventBridge Pipes is for point-to-point integration: one source, optional filter, optional enrichment, one target. Sources are DynamoDB streams, Kinesis streams, Amazon MQ brokers, Amazon MSK, SQS queues and Apache Kafka. Source order is preserved, and you are charged only for events that pass the filter. A pipe replaces the small "read, filter, call, forward" Lambda you would otherwise write.
SNSEventBridge (rules and targets)
RoutingFilter policy per subscription on attributes or bodyRule with rich patterns on the whole event
Typical sourcesYour own publishersAWS services, SaaS partners and your own code
Targets per fan-outVery large subscription countsUp to 5 targets per rule, up to 300 rules per bus by default
Throughput focusHigh publish rates, large fan-outMany event types and rules, moderate rates (regional quotas apply)
ReplayFIFO topic archive and replayArchive and replay on the bus
SchedulesNoEventBridge Scheduler
Pick it whenSame message to many consumers, mobile or email delivery, high rateRouting by content, reacting to AWS events, cross-account event buses, schedules
⚠ Using EventBridge as a high-volume data pipe
  • EventBridge is built for events and routing, and the account quotas for PutEvents and invocations are per Region. A firehose of telemetry belongs in Kinesis, MSK or SNS-to-SQS. Use EventBridge for the business events and AWS events that need content-based routing.
07
Chapter Seven - Streams

Kinesis Data Streams and Amazon MSK

Kinesis Data Streams (KDS) is AWS's native log. Like Kafka (page 02) it keeps ordered records for a retention period and lets many applications read them independently, but it is a different API, with shards instead of partitions and no brokers to run. Amazon MSK is the other route: managed Apache Kafka, with the Kafka API, clients and ecosystem unchanged.

Kinesis Data Streams: partition key to shard, shared vs enhanced fan-out readers
Producer PutRecords partition key is hashed to choose the shard Stream (3 shards) Shard 1 in 1 MB/s | out 2 MB/s Shard 2 in 1 MB/s | out 2 MB/s Shard 3 in 1 MB/s | out 2 MB/s Shared-throughput readers GetRecords (poll) all share 2 MB/s per shard Enhanced fan-out readers SubscribeToShard (push) 2 MB/s per shard EACH
Shards, partition keys and capacity
  • A stream is a set of shards. Each record has a partition key, a data blob and a sequence number assigned by Kinesis. The partition key is hashed to choose the shard, so all records with the same key go to the same shard and keep their order there.
  • Each shard supports up to 1 MB/s or 1,000 records/s for writes and 2 MB/s (and 5 read transactions per second) for reads. Stream capacity is the sum of its shards.
  • A record's data payload can be up to 10 MiB (before base64); the quotas page notes that the design target is intermittent large records of 1 to 10 MiB using burst capacity, not a steady stream of them.
  • Hot shards: a skewed partition key (one customer sending most traffic) overloads one shard while others idle. Symptom: WriteProvisionedThroughputExceeded on the stream even though total throughput looks low. Fix with a higher-cardinality key or a key salt.
Provisioned modeOn-demand mode
CapacityYou choose the shard count and change it with UpdateShardCount (or split and merge)Kinesis scales shards for you
ThroughputPer shard: 1 MB/s in, 2 MB/s outNew streams start at 4 MB/s write and 8 MB/s read, and scale to the account limits (10 GB/s write and 20 GB/s read in N. Virginia, Oregon and Ireland; 200 MB/s and 400 MB/s in other Regions; see the quotas page)
Billing shapePer shard-hour plus per data putPer throughput used (see the pricing page)
SwitchingYou can switch a stream between modes twice in 24 hours
Pick it whenSteady, predictable load you want to cost preciselySpiky or unknown load, no desire to manage shards

On-demand has two flavours in the current docs, Standard and Advantage; Advantage allows up to 50 registered enhanced fan-out consumers per stream instead of 20. Details and pricing are on the Kinesis pages.

Retention, readers and the KCL
  • Retention is 24 hours by default and can be raised to 365 days (8,760 hours) with IncreaseStreamRetentionPeriod (and lowered, to a minimum of 24 hours, with DecreaseStreamRetentionPeriod).
  • Shared-throughput readers call GetRecords; all of them divide a shard's 2 MB/s and its 5 read transactions per second. Add a third and fourth application and each gets less, and latency rises.
  • Enhanced fan-out gives each registered consumer a dedicated 2 MB/s per shard, pushed over HTTP/2 with SubscribeToShard, so readers do not compete. Up to 20 registered consumers per stream (50 with On-demand Advantage). It is billed separately.
  • The Kinesis Client Library (KCL) is a Java library (other languages through the MultiLangDaemon) that handles worker coordination, load balancing, checkpointing in a DynamoDB lease table, failover, and shard splits and merges. Delivery is at-least-once, so process idempotently. KCL 2.0 and later use enhanced fan-out. AWS recommends the latest version; the docs call out upgrading from KCL 2.6.0 or earlier to 2.6.1 or later.
Resharding

In provisioned mode you change capacity by splitting a hot shard into two or merging two cold ones (or with UpdateShardCount). The parent shard stops accepting writes and its data stays readable until retention ends; new records go to the child shards. The KCL processes all parent data before the children, which preserves per-partition-key order across a reshard. A hand-written consumer must do the same.

Watch GetRecords.IteratorAgeMilliseconds (how far behind the reader is), ReadProvisionedThroughputExceeded, WriteProvisionedThroughputExceeded and IncomingBytes.

Java - writing a batch to a stream (compiled against AWS SDK for Java v2)
import software.amazon.awssdk.core.SdkBytes; import software.amazon.awssdk.services.kinesis.KinesisClient; import software.amazon.awssdk.services.kinesis.model.PutRecordsRequestEntry; import software.amazon.awssdk.services.kinesis.model.PutRecordsResponse; import java.util.ArrayList; import java.util.List; public class StreamProducer { public static void send(KinesisClient kinesis, String streamName, List<String[]> keyAndJson) { List<PutRecordsRequestEntry> entries = new ArrayList<>(); for (String[] kv : keyAndJson) { entries.add(PutRecordsRequestEntry.builder() .partitionKey(kv[0]) // hashed to pick the shard: same key, same shard .data(SdkBytes.fromUtf8String(kv[1])) .build()); } PutRecordsResponse resp = kinesis.putRecords(r -> r.streamName(streamName).records(entries)); if (resp.failedRecordCount() > 0) { // The call itself succeeded; individual records can fail (throttling). Retry those entries. for (int i = 0; i < resp.records().size(); i++) { if (resp.records().get(i).errorCode() != null) { System.err.println("retry entry " + i + ": " + resp.records().get(i).errorCode()); } } } } }

PutRecords can succeed as a call while individual records fail (throttling on a hot shard). The response's failedRecordCount and per-record errorCode tell you which entries to retry, and retries can reorder records, so do not depend on cross-retry ordering.

Amazon MSK: what exists now
OptionWhat it isNotes (MSK guide, checked 2026-10-31)
MSK Provisioned, Standard brokersYou pick broker type, count per Availability Zone (minimum one broker per AZ) and storageClosest to self-managed Kafka without the servers. AWS runs the KRaft controllers at no extra cost
MSK Provisioned, Express brokersBrokers with pay-as-you-go storage that scales automatically, no storage sizingAWS describes up to 3x throughput per broker, faster scaling and recovery than Standard. Three-AZ only, select instance sizes, no maintenance windows. Supported on Kafka 3.6, 3.8, 3.9 and 4.2; KRaft from 3.9
MSK ServerlessA cluster type where AWS provisions and scales capacity and manages partitionsThroughput-based pricing. IAM access control only (Kafka ACLs are not supported). Available in a limited list of Regions

MSK runs open-source Apache Kafka, so producers and consumers from pages 03 and 04 work unchanged. Related managed pieces are MSK Connect (Kafka Connect) and MSK Replicator (cross-cluster replication for provisioned clusters). The set of supported Kafka versions changes, so check the "supported Apache Kafka versions" page before choosing.

MSK is the right answer when you already use Kafka (clients, Kafka Streams, Connect, a team that knows it) or you need Kafka's ecosystem and semantics, including consumer groups and transactions. KDS is simpler when you live in AWS and want nothing to operate.

⚠ Treating shard count as a one-time decision
  • In provisioned mode the shard count caps both write and read throughput, and adding shards later changes which shard a key maps to, so for a short while one key's records straddle a parent and a child shard. Plan the key, watch IteratorAgeMilliseconds, and reshard before consumers fall behind retention. If you do not want to think about it, on-demand mode exists for that reason.
08
Chapter Eight - Decision Guide

Kinesis vs MSK vs SQS (and SNS, EventBridge)

The services overlap, so choose by the question you are really asking: is each message a job to be done once (queue), a fact other systems may want to react to (topic, bus) or an ordered history to be read, re-read and processed as a stream (log)?

QuestionSQSKinesis Data StreamsMSK
Each message is processed by one worker, then it is doneBest fitAwkward (no per-message ack, a failure blocks the shard)Awkward
Several independent applications read the same dataNeeds SNS or EventBridge in front to copy itYes, shared or enhanced fan-outYes, consumer groups
Replay history after a bug or for a new consumerNoYes, within retention (to 365 days)Yes, within topic retention
Per-key ordering at high volumeFIFO queues, limited by group and API throughputYes, per shard by partition keyYes, per partition
Retry one bad message later without blocking othersYes, visibility timeout and DLQNo: it blocks the shard; bisect and skipNo: needs a retry topic
Delay or schedule deliveryDelay up to 15 minutesNoNo
Stream processing (windows, joins)NoManaged Service for Apache Flink, KCLKafka Streams, Flink
Existing Kafka clients, tools or connectorsNoNo (different API)Yes
Operations burdenNoneLow (shards unless on-demand)Higher (brokers, partitions, upgrades) except Serverless
Message size limit1 MiB (2 GB with the Extended Client)Up to 10 MiB per recordPer Kafka and broker settings
Rules of thumb
  • Default to SQS for asynchronous work: it is the least to operate, scales without thinking, and its visibility timeout, DLQ and redrive handle failure well.
  • SNS plus one SQS queue per consumer when several teams need the same event and each needs its own retries, backlog and failure isolation.
  • EventBridge when the routing depends on event content, when the events come from AWS services or SaaS, or when you need schedules and cross-account delivery.
  • Kinesis when you need an ordered, replayable stream with several readers and prefer AWS-native, serverless operation.
  • MSK when you need Kafka itself: its ecosystem, its consumer-group and transaction semantics, or portability with a Kafka estate elsewhere (pages 02 to 04).
  • They compose: a common shape is producers to Kinesis or MSK for the raw stream, a Lambda or Flink job turning it into business events on EventBridge, and SNS to SQS delivering those events to services.
⚠ Choosing a stream because it sounds scalable
  • A team picks Kinesis for a job queue, then finds that one poison record stops its shard, there is no per-message delay or DLQ semantic, and parallelism is capped by shards. Conversely, a team builds an event-history store on SQS and discovers messages are gone once consumed. Decide queue versus log first, product second.
Cost mental model (qualitative on purpose)
  • SQS and SNS bill mostly per request (and for data transfer), so batching and long polling reduce cost directly. EventBridge bills per event (plus extra for archives, Pipes and Scheduler). Kinesis provisioned bills per shard-hour plus data, on-demand per throughput, and enhanced fan-out is extra. MSK bills per broker and storage, or per throughput on Serverless and Express storage. Prices differ by Region and change, so use the AWS pricing pages and the pricing calculator with your own message counts rather than a rule of thumb.
09
Chapter Nine - Lambda

Event Source Mappings for SQS, Kinesis and MSK

When a Lambda function consumes a queue or stream, Lambda itself does the polling. An event source mapping is the Lambda-managed resource that reads from the source, gathers a batch and invokes your function synchronously with it. You write a handler that receives a list of records, not a poller.

Lambda event source mapping on SQS: Lambda polls, batches and invokes
SQS queue Event source mapping (run by Lambda) long-polls the queue builds batches scales pollers Invocation 1: batch of N Invocation 2: batch of N Invocation 3: batch of N Response batchItemFailures = [] : Lambda deletes the whole batch. Response lists message IDs : only the others are deleted, the listed ones return after the visibility timeout. Function throws : the entire batch returns to the queue. Queue visibility timeout should be at least 6x the function timeout (plus the batch window).
SQS as the source
  • Batch size: up to 10,000 for a standard queue, 10 for FIFO. Over 10 requires a batch window (MaximumBatchingWindowInSeconds) of at least 1 second; the window applies to standard queues only. A batch is also bounded by the 6 MB synchronous payload limit, so you may get fewer records than the batch size.
  • Visibility timeout: the function timeout must not exceed the queue's visibility timeout (Lambda checks), and AWS recommends six times the function timeout (plus the batch window) so retries after throttling do not outrun it.
  • DLQ: put the redrive policy on the source queue (an SQS DLQ), not on the Lambda function. Set maxReceiveCount to at least 5.
  • Encrypted queue: add kms:Decrypt to the function's execution role. The managed policy AWSLambdaSQSQueueExecutionRole covers the queue permissions.

Concurrency scaling (standard queues): Lambda starts with five concurrent batches and adds up to 300 more concurrent invocations per minute, to a maximum of 1,250 for one mapping. When traffic is low it scales back toward five (and as few as two). Maximum concurrency on the mapping caps how many instances the queue may use, protecting a shared account pool or a downstream database. Provisioned mode (a minimum and maximum number of dedicated event pollers) scales faster and higher at extra cost, and cannot be combined with maximum concurrency. If you set reserved concurrency on the function, leave at least five, or the mapping is throttled.

FIFO: Lambda delivers messages in order per message group, and a failure holds the group until retries are done. Different groups run in parallel.

Partial batch responses (ReportBatchItemFailures)

Without it, one bad record fails the whole batch: every message, including the good ones, returns to the queue and is processed again. Enable ReportBatchItemFailures in the mapping's FunctionResponseTypes, catch exceptions per record, and return the IDs of the ones that failed. Everything not listed is treated as successful and deleted.

Java - an SQS batch handler with partial failure reporting (compiled against aws-lambda-java-events)
import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse; import com.amazonaws.services.lambda.runtime.events.SQSEvent; import java.util.ArrayList; import java.util.List; public class OrderBatchHandler implements RequestHandler<SQSEvent, SQSBatchResponse> { @Override public SQSBatchResponse handleRequest(SQSEvent event, Context context) { List<SQSBatchResponse.BatchItemFailure> failures = new ArrayList<>(); for (SQSEvent.SQSMessage message : event.getRecords()) { try { process(message.getBody()); } catch (Exception e) { failures.add(new SQSBatchResponse.BatchItemFailure(message.getMessageId())); // For a FIFO queue: stop here and also report every remaining message, to keep order. } } return new SQSBatchResponse(failures); // empty list = whole batch succeeded } private void process(String body) { /* business logic, idempotent */ } }
  • Return an empty list for full success. If the handler throws, the whole batch counts as failed.
  • For a FIFO queue, stop at the first failure and report all remaining messages in the batch too, or you break the order.
  • With the feature on, Lambda does not scale polling down when invocations fail, which keeps throughput up while a few messages are bad.
  • The Powertools for AWS Lambda Batch Processor (Python, Java, TypeScript, .NET) implements this protocol for you.
Shell - enable it on an existing mapping (UUID from list-event-source-mappings)
$ aws lambda update-event-source-mapping --uuid "$MAPPING_UUID" \ --function-response-types "ReportBatchItemFailures"
Kinesis and MSK as the source
Kinesis Data StreamsMSK (and self-managed Kafka)
Ordering and scaling unitOne batch in flight per shard; ParallelizationFactor 1 (default) to 10 allows several concurrent batches per shard (same partition key order kept)Partitions; event pollers scale with backlog (provisioned mode: a poller handles up to 5 MB/s or 5 concurrent invocations)
Reader typeShared (polls each shard about once per second) or enhanced fan-out consumer for dedicated throughputKafka consumer group managed by Lambda
BatchingBatch size and window (default window 0 s, so invoke as soon as records arrive); batch limited by 6 MBWindow 0 to 300 s
On errorBy default the shard is blocked: the batch is retried until it succeeds or the records expire, so one bad record stalls the shardRetries until success or record age, configurable
ControlsBisectBatchOnFunctionError, MaximumRetryAttempts, MaximumRecordAgeInSeconds, DestinationConfig on-failure destination, ReportBatchItemFailures (return the sequence number)Maximum retry attempts (default -1, infinite; 10 if you set an on-failure destination), maximum record age, split batch on error, partial batch response, Kafka topic as on-failure destination
Lag metricLambda IteratorAgeLambda OffsetLag
  • Starting position: with LATEST, events can be missed while the mapping is created or updated; use TRIM_HORIZON if you must not miss any.
  • Bisect on error halves a failing batch to isolate the bad record; combined with ReportBatchItemFailures it bisects at the returned sequence number.
  • A stream consumer must configure retry limits and an on-failure destination, otherwise a single poison record can block a shard until it expires from the stream. This is the biggest operational difference from SQS, where the DLQ is the default escape hatch.
⚠ Function timeout longer than the visibility timeout
  • Lambda rejects a mapping whose function timeout exceeds the queue visibility timeout, but a timeout that merely equals it still causes duplicates when a batch is retried after throttling. Set the visibility timeout to 6x the function timeout plus the batch window, and expect the occasional duplicate anyway: handlers must be idempotent (next chapter).
10
Chapter Ten - Running It

Security, Idempotency, Monitoring and Cost

IAM policies and queue policies

SQS access is the union of two policy systems: identity-based IAM policies attached to the caller, and the queue policy attached to the queue (a resource-based policy). A cross-account caller or an AWS service needs the queue policy to allow it. ListQueues can only be allowed in an IAM policy; every other SQS action supports resource-level permissions, so name the queue ARN.

RoleNeedsWhere
Producersqs:SendMessage on the queue ARNProducer role (IAM policy)
Consumersqs:ReceiveMessage, sqs:DeleteMessage, sqs:ChangeMessageVisibility, sqs:GetQueueAttributesConsumer role (IAM policy)
SNS topic delivering to a queuesqs:SendMessage for principal sns.amazonaws.com, conditioned on aws:SourceArn = the topic ARNQueue policy
EventBridge rule targeting a queuesqs:SendMessage, via an execution role (RoleArn on the target) that EventBridge assumes (trust: events.amazonaws.com)IAM role on the target
Lambda reading a queueManaged policy AWSLambdaSQSQueueExecutionRole; add kms:Decrypt for an encrypted queueFunction execution role
DLQ redrivesqs:StartMessageMoveTask, ReceiveMessage, DeleteMessage, GetQueueAttributes on the DLQ; SendMessage on the destinationOperator role

Policies have size limits (8,192 bytes, 20 statements, 50 principals, 10 conditions per the quotas page), so a queue shared by dozens of topics needs a wildcard condition on aws:SourceArn or a different design.

⚠ SNS cannot deliver to the queue and nothing tells you
  • A subscription is confirmed but nothing arrives, because the queue policy does not allow the topic. This is a client-side error: SNS does not retry it, and it is discarded unless the subscription has a DLQ. Check NumberOfNotificationsFailed on the topic and add a DLQ to every important subscription.
Encryption: SSE-SQS vs SSE-KMS
SSE-SQSSSE-KMS
KeysSQS-managed keysAWS managed key alias/aws/sqs or your own customer managed KMS key
Extra setupNoneKey policy and IAM must allow producers and consumers (kms:GenerateDataKey, kms:Decrypt)
Control and auditLimitedKey policy, rotation and CloudTrail on key use
Cost and limitsNo KMS callsKMS requests (reduced by the data key reuse period: default 5 minutes, 1 minute to 24 hours) and KMS throttling at high rates
Works with SNS to SQSYesOnly with a customer managed key whose key policy grants the SNS service principal GenerateDataKey and Decrypt
  • SQS has server-side encryption enabled by default on newly created queues. It encrypts the message body only: queue name and attributes, message ID, timestamp and message attributes are not encrypted.
  • Only messages sent after encryption is enabled are encrypted; the existing backlog is not re-encrypted.
  • All requests to an encrypted queue must use HTTPS and Signature Version 4, and anonymous send or receive is rejected.
  • A queue encrypted with the default AWS managed key cannot invoke a Lambda function in a different account. Use a customer managed key for cross-account Lambda.
  • Kinesis, SNS and EventBridge (custom bus) support encryption at rest with KMS too; check each service's page for the key-policy details.
Idempotency: assume every message arrives twice

SQS standard, SNS, EventBridge, Kinesis (KCL) and Lambda retries are all at-least-once, and visibility timeouts, redrive, replay and ESM retries create duplicates on purpose. FIFO deduplication helps only within its 5-minute window and only on the send side. Correctness therefore comes from the consumer:

  • Derive an idempotency key from the message (an order ID plus event type, or the message ID) and record it in the same transaction as the side effect (a unique constraint, or a DynamoDB conditional write that fails if the key exists).
  • Prefer naturally idempotent operations: set a field to a value, upsert by key, rather than increment or append.
  • For Lambda, the Lambda docs point to Powertools for AWS Lambda, which provides an idempotency utility, and a Batch Processor for partial failures. It is a convenience, not a requirement; the pattern above works without it.
  • Use the same idea for external calls: pass an idempotency key to the payment or email provider so a retried message does not charge or mail twice.
Monitoring: the metrics that matter
ServiceMetricMeaning and use
SQSApproximateNumberOfMessagesVisibleBacklog waiting for a consumer. Scale consumers on it; alarm > 0 on a DLQ
SQSApproximateAgeOfOldestMessage (seconds)How long the oldest message has waited: the "are we keeping up" signal. Caveat: poison messages are excluded after repeated receives (chapter 03)
SQSApproximateNumberOfMessagesNotVisibleIn flight. A high steady value means stuck or slow consumers or visibility timeouts that are too long
SQSNumberOfMessagesSent, NumberOfMessagesReceived, NumberOfMessagesDeletedReceived far above deleted means failures and redelivery
SQSNumberOfEmptyReceivesWasteful polling (short polling or too many idle consumers)
SQS FIFOApproximateNumberOfGroupsWithInflightMessagesHow many lanes are busy; low with a big backlog means a group bottleneck
SNSNumberOfMessagesPublished, NumberOfNotificationsDelivered, NumberOfNotificationsFailedPublish volume, delivery success and failure per topic
SNSNumberOfNotificationsRedrivenToDlq, ...FailedToRedriveToDlqFailures going to the subscription DLQ (and failing to)
EventBridgeMatchedEvents, Invocations, FailedInvocations, ThrottledRulesRule matches, target deliveries, permanent delivery failures, throttling
EventBridgeInvocationsSentToDlq, InvocationsFailedToBeSentToDlqEvents going to (or failing to reach) the target DLQ
KinesisGetRecords.IteratorAgeMillisecondsHow far consumers are behind the tip. Alarm well before retention
KinesisWriteProvisionedThroughputExceeded, ReadProvisionedThroughputExceededThrottled records or reads: add shards, fix a hot key, or use enhanced fan-out
Lambda (stream sources)IteratorAge (Kinesis), OffsetLag (MSK)Lag of the event source mapping
LambdaThrottles, ConcurrentExecutionsThrottling of the function that reads your queue

SQS metrics are approximate (a distributed system) and some are emitted only while the queue is active, so alarm on several consecutive data points, not a single one. For MSK, use the cluster metrics in the MSK monitoring documentation (consumer lag is the one you will want), not the Kinesis ones.

Cost mental model
  • SQS and SNS are request-priced. Cost scales with the number of API calls and messages, so batching (up to 10 per call) and long polling (fewer empty receives) cut the bill, and so does not polling from many idle workers.
  • EventBridge is event-priced: every event placed on a bus and delivered counts; archives, Pipes and Scheduler have their own meters. A chatty producer on a bus costs more than the same traffic on SNS.
  • Kinesis provisioned is capacity-priced: you pay for shard-hours whether or not data flows, plus data put and enhanced fan-out reads. On-demand trades that for per-throughput pricing.
  • MSK is infrastructure-priced (brokers and storage), or throughput-priced on Serverless.
  • Big hidden costs are elsewhere: data transfer across AZs and Regions, KMS requests on encrypted high-volume queues, Lambda duration and retries, CloudWatch Logs. Model your real volume in the pricing calculator rather than quoting a per-million figure.
11
Chapter Eleven - Hands-On

Java SDK v2: A Resilient SQS Worker, and Common Mistakes

Application code for SQS in Java uses the AWS SDK for Java v2: software.amazon.awssdk:sqs, and the SqsClient class. SqsClient.create() uses the default Region and credential provider chain (environment, shared config, container or instance role). The class below was compiled with javac against SDK v2 2.31.0; it was not run against a live queue, so treat it as an exact API reference and test it in a sandbox account.

Java - OrderWorker: batch send, long-poll receive loop, batch delete, visibility extension
import software.amazon.awssdk.services.sqs.SqsClient; import software.amazon.awssdk.services.sqs.model.*; import java.util.ArrayList; import java.util.List; public class OrderWorker { private final SqsClient sqs = SqsClient.create(); // region and credentials from the default provider chain private final String queueUrl; public OrderWorker(String queueUrl) { this.queueUrl = queueUrl; } /** Send up to 10 messages in one request. Check the per-entry result: a batch can partly fail. */ public void sendBatch(List<String> bodies) { List<SendMessageBatchRequestEntry> entries = new ArrayList<>(); for (int i = 0; i < bodies.size(); i++) { entries.add(SendMessageBatchRequestEntry.builder() .id("e" + i) // unique within this request, used to match results .messageBody(bodies.get(i)) .build()); } SendMessageBatchResponse resp = sqs.sendMessageBatch(b -> b.queueUrl(queueUrl).entries(entries)); for (BatchResultErrorEntry err : resp.failed()) { System.err.println("send failed for " + err.id() + ": " + err.code() + " " + err.message()); // retry just these entries (or give up and surface the error) } } /** The consumer loop: long poll, process, delete only what succeeded. */ public void run() { while (!Thread.currentThread().isInterrupted()) { ReceiveMessageResponse resp = sqs.receiveMessage(r -> r .queueUrl(queueUrl) .maxNumberOfMessages(10) // the maximum per request .waitTimeSeconds(20) // long polling, 20 s is the maximum .messageSystemAttributeNames(MessageSystemAttributeName.APPROXIMATE_RECEIVE_COUNT)); List<DeleteMessageBatchRequestEntry> done = new ArrayList<>(); for (Message m : resp.messages()) { try { handle(m); done.add(DeleteMessageBatchRequestEntry.builder() .id(m.messageId()) .receiptHandle(m.receiptHandle()) .build()); } catch (Exception e) { // Do not delete: the message reappears after the visibility timeout // and moves to the DLQ once maxReceiveCount is exceeded. System.err.println("failed " + m.messageId() + ": " + e); } } if (!done.isEmpty()) { sqs.deleteMessageBatch(b -> b.queueUrl(queueUrl).entries(done)); } } } private void handle(Message m) throws Exception { // A slow step? Ask for more time before the visibility timeout runs out. sqs.changeMessageVisibility(r -> r .queueUrl(queueUrl) .receiptHandle(m.receiptHandle()) .visibilityTimeout(120)); // seconds from now, not added to the old value System.out.println("processing " + m.body()); } }
  • waitTimeSeconds(20) makes this a long poll; with the loop it costs almost nothing while idle.
  • maxNumberOfMessages(10) is the maximum per receive. messageSystemAttributeNames(APPROXIMATE_RECEIVE_COUNT) returns how many times SQS has delivered the message, useful for logging near the DLQ threshold.
  • Messages are deleted only after success, in one deleteMessageBatch call. A failed message is left alone: the visibility timeout returns it, and maxReceiveCount eventually moves it to the DLQ.
  • sendMessageBatch returns per-entry results. Retry entries from failed(); do not assume the call is all-or-nothing.
  • In Spring Boot you would normally use a framework (for example Spring Cloud AWS listeners) instead of a hand-rolled loop. The loop shows exactly what those frameworks do: poll, process, delete, extend.
Shell - a throwaway queue to try it (needs credentials; output omitted)
$ aws sqs create-queue --queue-name demo-orders --attributes ReceiveMessageWaitTimeSeconds=20,VisibilityTimeout=60 $ aws sqs send-message --queue-url "$QUEUE_URL" --message-body '{"orderId":"o-1"}' $ aws sqs receive-message --queue-url "$QUEUE_URL" --wait-time-seconds 20 --max-number-of-messages 10 $ aws sqs get-queue-attributes --queue-url "$QUEUE_URL" --attribute-names All $ aws sqs delete-queue --queue-url "$QUEUE_URL"
Common mistakes (what you see, and the fix)
MistakeWhat you seeFix
Visibility timeout shorter than processing timeDuplicate processing, rising NumberOfMessagesReceived, messages reaching the DLQ although the code worksTimeout above worst-case processing; heartbeat with ChangeMessageVisibility; 6x function timeout for Lambda
Short pollingMany empty receives, a bigger bill, occasionally "missing" messagesLong polling, 20 seconds
No alarm on the DLQFailed messages silently expire; discovered days laterAlarm ApproximateNumberOfMessagesVisible > 0 on every DLQ, DLQ retention longer than source
Single FIFO message groupIdle consumers, growing backlog, 1 busy groupGroup by the entity needing order; many groups
maxReceiveCount of 1One transient error sends good messages to the DLQAt least 3 to 5; at least 5 for Lambda
Lambda without ReportBatchItemFailuresOne bad message reprocesses the whole batchReturn batchItemFailures
Not idempotentDouble emails, double charges after retries and redriveIdempotency key plus a unique constraint
Stream consumer with unlimited retriesOne poison record blocks a shard; IteratorAge climbsRetry limits, bisect, record age limit, on-failure destination
SNS without queue policy or DLQSubscription "works" but delivers nothing; messages discardedaws:SourceArn queue policy, DLQ on subscription
Hot partition key on KinesisWrite throttling at low total throughputHigher-cardinality key or salt
⚠ Deleting on receive instead of on success
  • A consumer that calls deleteMessage as soon as it gets the message turns SQS into at-most-once: a crash mid-processing loses the message. Delete only after the work and its side effects are done, and make the work safe to repeat.
12
Chapter Twelve - Interview

Interview Questions and Answers

  • Q: Standard vs FIFO queue?
    Standard: nearly unlimited throughput, at-least-once, best-effort ordering. FIFO: ordering per message group and deduplication within a 5-minute window, with lower per-partition throughput (300 transactions per second per action, more with batching and high throughput mode). Choose FIFO only when order or duplicate suppression is a real requirement.
  • Q: What is the visibility timeout and what happens if it is too short?
    It hides a received message from other consumers (default 30 s, max 12 h from first receive). If processing outlasts it, the message is delivered again, causing duplicates and extra receives that can send it to the DLQ. Set it above worst-case processing, or extend it with ChangeMessageVisibility.
  • Q: Short vs long polling?
    Short polling samples a subset of servers and returns immediately, often empty. Long polling (WaitTimeSeconds up to 20) queries all servers and waits for a message, reducing empty responses, cost and latency.
  • Q: How does a DLQ work and how do you redrive?
    A redrive policy on the source queue names the DLQ and maxReceiveCount. After that many receives without a delete, SQS moves the message. After fixing the bug, StartMessageMoveTask (console: DLQ redrive) moves messages back to the source or another queue of the same type, at a velocity you can limit.
  • Q: How do message groups give ordering and how can they hurt throughput?
    Messages in a group are delivered one at a time in order. Different groups run in parallel. A single group ID serialises the whole queue, and a failing message blocks its group.
  • Q: How does FIFO deduplication work?
    A MessageDeduplicationId (or a hash of the body with content-based deduplication) identifies a message. A second message with the same ID within 5 minutes is accepted but not delivered. It protects against producer retries, not against consumer-side duplicates.
  • Q: What is the SNS to SQS fan-out and why use it?
    One publish to a topic, a queue subscribed per consumer. Each consumer gets its own durable backlog, retry and DLQ, and a slow consumer does not affect others. Needs a queue policy letting SNS send, and usually raw message delivery to skip the SNS envelope.
  • Q: What are SNS filter policies?
    JSON per subscription that selects messages by attributes (or body, with FilterPolicyScope=MessageBody), so subscribers receive only what they need. Evaluated by SNS before delivery. Changes can take up to 15 minutes to apply.
  • Q: What happens when an SNS delivery fails?
    Server-side errors are retried per the endpoint policy (SQS and Lambda: up to 100,015 attempts over 23 days). Client-side errors are not retried. After exhaustion the message is discarded unless the subscription has a DLQ (an SQS queue).
  • Q: SNS vs EventBridge?
    SNS: high-rate fan-out of the same message to many subscribers with attribute or body filters. EventBridge: content-based routing with rich patterns, AWS and SaaS event sources, input transformation, archive and replay, schedules and cross-account buses, at moderate rates and up to 5 targets per rule.
  • Q: What are EventBridge Pipes and Scheduler?
    Pipes: point-to-point source to target with filtering and enrichment, no glue code. Scheduler: cron, rate and one-time schedules that call targets, with retries and flexible time windows.
  • Q: Kinesis vs SQS?
    SQS is a queue: each message is processed by one consumer and removed, with per-message retry and DLQ. Kinesis is a log: ordered per shard, replayable within retention, read by many applications independently, but a failing record blocks its shard.
  • Q: Kinesis vs MSK?
    Both are logs. Kinesis is AWS-native with shards and no brokers; MSK is managed Apache Kafka with the full Kafka API and ecosystem. Pick MSK to keep Kafka tooling and semantics, Kinesis when you want the simplest AWS-native stream.
  • Q: What is a shard and what causes a hot shard?
    The unit of capacity (1 MB/s or 1,000 records/s in, 2 MB/s out). Records with one partition key land on one shard, so a skewed key concentrates load and throttles writes. Fix with a better key, a salt or more shards.
  • Q: What is enhanced fan-out?
    A registered consumer gets a dedicated 2 MB/s per shard pushed over HTTP/2, instead of sharing the shard's 2 MB/s with other GetRecords readers.
  • Q: How does Lambda consume SQS?
    An event source mapping polls the queue, batches messages (up to 10,000 for standard with a window, 10 for FIFO) and invokes the function. It scales pollers up to 1,250 concurrent invocations, or fewer with maximum concurrency. Failures return the batch unless ReportBatchItemFailures is used.
  • Q: What is ReportBatchItemFailures?
    A setting that lets the handler return batchItemFailures with the IDs of failed messages, so only those return to the queue. Not listing an ID means success. For FIFO, stop at the first failure and report the rest too.
  • Q: How do you handle a poison record in a Kinesis Lambda consumer?
    Retry limit and maximum record age, bisect the batch, partial batch response by sequence number, and an on-failure destination so the failed batch metadata is saved instead of blocking the shard.
  • Q: SSE-SQS vs SSE-KMS?
    SSE-SQS uses SQS-managed keys with no setup. SSE-KMS uses an AWS managed or customer managed KMS key, with key policy control and audit but KMS calls (reduced by the data key reuse period). SNS to an encrypted queue needs a customer managed key with a policy for the SNS principal.
  • Q: How do you make consumers idempotent?
    Store an idempotency key with the side effect in one transaction (unique constraint or conditional write), prefer upserts, and pass the key to external APIs. Powertools for AWS Lambda has an idempotency utility.
  • Q: How do you send a payload bigger than 1 MiB through SQS?
    The Extended Client Library (Java, Python) stores the body in S3 (up to 2 GB) and sends a reference; both sides must use it, and it supports synchronous clients only.
  • Q: Which CloudWatch metrics would you alarm on?
    DLQ ApproximateNumberOfMessagesVisible > 0, SQS ApproximateAgeOfOldestMessage, Kinesis GetRecords.IteratorAgeMilliseconds, throttling metrics, SNS NumberOfNotificationsFailed, EventBridge FailedInvocations.
  • Q: Scenario: orders must reach billing, shipping and analytics, each with its own failure handling. Design it.
    Order service publishes OrderPlaced to an SNS topic. Three SQS queues subscribe with raw delivery and filter policies, each with a DLQ and DLQ alarm. Consumers (Lambda with partial batch responses, or a worker) are idempotent. Add a subscription DLQ on the topic. Use a FIFO topic and FIFO queues only if per-order ordering is required.
  • Q: Scenario: a Lambda reading a queue makes duplicates under load. Why?
    Visibility timeout too close to the function timeout, throttling, or a whole batch failing on one bad message. Set visibility to 6x function timeout (plus batch window), enable ReportBatchItemFailures, limit concurrency, and make processing idempotent.
  • Q: Scenario: a stream consumer is falling behind. What do you check?
    IteratorAgeMilliseconds, per-shard throughput, hot shard, consumer errors blocking a shard, enhanced fan-out for contention, parallelization factor, function duration, then shard count or on-demand mode.
13
Chapter Thirteen - Summary

Cheat Sheet and Recap

Cheat sheet
ConceptRememberValue (verified 2026-10-31)
SQS message sizeBigger needs the Extended Client1 MiB (2 GB via S3)
SQS retentionSet the DLQ higher than the source4 days default, 60 s to 14 days
Visibility timeoutAbove processing time; heartbeat to extend30 s default, up to 12 h from first receive
Long pollingAlways onWaitTimeSeconds up to 20 s
BatchCheck per-entry failuresUp to 10 per call, 1 MiB total
DelayNot a scheduler0 to 15 minutes
FIFO throughputPer partition, per API action300 TPS; 3,000 msgs/s batched; high throughput by Region
FIFO dedup windowSend-side only5 minutes
DLQAlarm on it, same type, account, RegionmaxReceiveCount (at least 5 for Lambda)
SNS message sizeTopic attribute raises it256 KiB default, 1 MiB max (Extended Client beyond)
SNS retriesSQS/Lambda: very long; client errors not retried100,015 attempts over 23 days (AWS managed endpoints)
SNS filter policyScope attributes by default; 15 minutes to apply200 per topic
EventBridgeRules: up to 5 targets; retry 24 h, 185 timesArchive and replay; Scheduler; Pipes
Kinesis shard1 MB/s in, 2 MB/s outRetention 24 h to 365 days
Kinesis fan-outEnhanced fan-out is dedicated per consumer20 consumers (50 On-demand Advantage)
Lambda + SQSVisibility 6x function timeout; partial batchBatch up to 10,000 (FIFO 10); to 1,250 concurrent
MSKProvisioned (Standard, Express) or ServerlessServerless: IAM auth only
∑ AWS messaging - what to remember
  • SQS = queue, SNS = broadcast, EventBridge = content router, Kinesis/MSK = log. Pick queue vs log first.
  • SQS delivers at-least-once and only hides a message when received. Delete after success, set the visibility timeout above processing time.
  • Always long-poll, always batch, always have a DLQ with an alarm and a retention longer than the source.
  • FIFO ordering is per message group; one group is one lane. Choose the group ID deliberately.
  • SNS to SQS gives each consumer a durable backlog. Use raw delivery, a scoped queue policy and a subscription DLQ.
  • EventBridge routes by content and handles AWS events, schedules and replay; it is not a high-volume data pipe.
  • Streams block on failure. Configure retry limits, bisect and on-failure destinations; watch iterator age.
  • Lambda ESM: visibility 6x timeout, ReportBatchItemFailures, maximum concurrency, idempotent handlers.
  • Everything is at-least-once. Idempotency is the consumer's job. Encrypt with SSE and mind KMS key policies for SNS and cross-account.
Version note

Limits, defaults and behaviours on this page were checked on 2026-10-31 against docs.aws.amazon.com: the SQS Developer Guide (message and FIFO quotas, visibility timeout, polling, DLQ and redrive, fair queues, encryption, CloudWatch metrics), the SNS Developer Guide and General Reference quotas (message size, filter policies, delivery retries, DLQs, FIFO topics), the EventBridge User Guide and Scheduler guide (patterns, retries, archive, Pipes, the new Custom Event Bus), the Kinesis Data Streams guide (retention, capacity modes, quotas, enhanced fan-out, KCL), the MSK Developer Guide (Provisioned, Express, Serverless) and the Lambda Developer Guide (SQS, Kinesis and MSK event source mappings). Not quoted on purpose: prices. The Java samples compile against AWS SDK for Java v2 2.31.0 but were not run against live AWS. Quotas differ by Region and change; re-check the quotas page for yours.

Next in this section