Skip to content

SQS: receive, process, delete, or retry

10 min read · main part only; folded sections are optional

After this you can answer: How do you process queued work safely when messages, workers and downstream services can fail?

The email queue sits between publication and processing. An event source mapping is a Lambda configuration that runs AWS pollers to receive messages and invoke the function. Its permissions come from the function’s execution role: the IAM identity used while Lambda runs the worker.

flowchart LR
SNS["minitickets-dev-booking-events"]:::aws -->|"delivers JSON — subscription + sqs:SendMessage queue policy"| Q["minitickets-dev-jobs"]:::service
Q -->|"polls — event source mapping; worker-role sqs:ReceiveMessage"| W["minitickets-dev-worker"]:::app
W -->|"mapping deletes successes — worker-role sqs:DeleteMessage"| Q
Q -->|"exhausted receives — redrivePolicy maxReceiveCount 3; byQueue allow"| D["minitickets-dev-jobs-dlq"]:::failure

Legend: highlighted = SQS queue; app = MiniTickets worker; AWS = managed service; red = failed work.

The delivery policy additionally requires aws:SourceArn to equal our topic and aws:SourceAccount to equal our account. These arrows match the shared map’s topic-to-jobs-queue, jobs-queue-to-worker and jobs-queue-to-dlq connections. Open in the map →.

  1. SNS sends the versioned booking JSON to the queue. SQS stores it and acknowledges acceptance.
  2. Lambda’s poller receives a batch. Each message gets a receipt handle, a value for this particular receive that permits deletion or a visibility change. A later receive has a new handle.
  3. SQS hides received messages for the visibility timeout, the period during which another consumer normally cannot receive them. They remain stored in the queue.
  4. Our worker validates each body, checks its business job key, sends an email and records completion. Messages in one invocation run sequentially.
  5. The handler returns batchItemFailures, a list of failed SQS message IDs. The mapping has ReportBatchItemFailures enabled, so Lambda deletes successful messages and leaves failures.
  6. Failed messages become visible after their wait. The queue’s redrive policy moves messages exceeding maxReceiveCount to its dead-letter queue, or DLQ: another queue holding failed work. Our setting permits three source-queue receives.
sequenceDiagram
box rgb(237,233,254) SQS
participant Q as minitickets-dev-jobs
end
participant P as Lambda poller
participant W as minitickets-dev-worker
participant D as jobs-dlq
P->>Q: ReceiveMessage, hide batch for 60 s
Q-->>P: Bodies + receipt handles
P->>W: Invoke with up to 10 records
W-->>P: batchItemFailures: failed IDs
P->>Q: DeleteMessage for successful records
Note over Q,W: Failed records wait, then become visible
P->>Q: Receive failed record again
Q->>D: Receive count exceeds 3: redrive

Legend: solid arrows = operations; dashed arrows = responses; the note is a wait, not deletion.

Without partial reporting, an escaped exception retries the whole batch. Returning a failure list without enabling the mapping setting is worse: Lambda treats a normal return as success and may delete failed work. A timeout also fails the whole invocation because no valid partial response reaches Lambda. AWS partial batch responses.

SQS standard queues store redundant message copies across Availability Zones, which are separate failure locations in one Region. The service can acknowledge storage before your consumer runs. This lets the API and the worker operate independently. A temporary worker outage need not become an API outage. AWS standard queue storage.

The distributed storage model provides at-least-once delivery: a message can be delivered more than once. For example, a stored copy may not receive a delete, or your worker may complete its side effect before its response is lost. Ordering is best effort. Treat both duplicate and reordered events as normal inputs, rather than surprising exceptions.

A FIFO queue, meaning first in, first out, adds ordering within a MessageGroupId. That field names the sequence whose messages must stay ordered. Different groups can run concurrently; one shared group limits parallelism. The producer also supplies a MessageDeduplicationId, or enables body-based deduplication, so repeated sends within a five-minute interval do not add another copy. Body-based deduplication hashes the body, not message attributes. AWS FIFO deduplication.

FIFO does not atomically commit an external email or payment together with queue deletion. A worker can still finish the action and crash before deletion. Idempotency means repeated execution produces the same intended result. It belongs in the business operation even when FIFO is selected. For ordered partial batches, stop after a failure and report failed and unprocessed records, preserving order. AWS ordered failure handling.

Long polling makes a receive request wait for messages instead of immediately returning empty. It reduces empty requests and false empty results. Lambda performs polling for us; our local runner supplies the equivalent loop. Queue storage, polling and execution are separate responsibilities. AWS long polling.

These values are at the time of writing; check the AWS docs before sizing production. Verified on 19 October 2026. A MiniTickets setting is a committed configuration, not a measured AWS throughput result.

ItemAWS value or recommendationMiniTickets / source
Message bodyUp to 1 MiB, rather than the older 256 KiB limitMessage quotas
RetentionDefault 4 days; range 1 minute–14 daysMain queues 4 days; DLQs 14 days; quotas
VisibilityDefault 30 seconds; maximum 12 hoursJobs 60 seconds; tickets 180 seconds; quotas
Long pollMaximum 20 secondsBoth queues use 20 seconds; long polling
SQS API batchUp to 10 messages per batch requestDistinct from Lambda’s batch setting; quotas
Lambda mapping batchUp to 10,000 standard / 10 FIFO, constrained by invocation payloadJobs uses 10; tickets 1; mapping configuration
Lambda visibility recommendationAt least 6 × function timeout + batching windowJobs 6 × 10 seconds; tickets 6 × 30 seconds; AWS guidance
In-flight messagesApproximately 120,000 per standard queue; quota increase availableReceived but not deleted; visibility guide

The six-times value provides retry time for throttled invocations. Creation validates that function timeout does not exceed visibility timeout; that minimum validation is weaker than the recommendation. A larger Lambda batch may require multiple SQS receives, and metadata counts toward Lambda’s invocation payload. AWS configuration details.

This path is built in source. The committed dataEnabled, computeEnabled and entryEnabled switches are currently false; built here means implemented in the repository, not running now. This page does not claim live AWS verification. infra/src/jobs/queues.ts exports createJobQueues, used for minitickets-dev-jobs and minitickets-dev-tickets, each with its own DLQ. Encryption at rest uses SSE-SQS: SQS-managed encryption that does not require our worker to decrypt through KMS.

infra/src/jobs/queue-trigger.ts configures up to five concurrent email batches. The ticket mapping in infra/src/tickets/lambda.ts allows two concurrent invocations with one record each. Keeping consumers separate lets ticket generation continue when email delivery fails.

apps/worker/src/batch.ts exports handleBatch. It catches errors per message and returns their IDs. apps/worker/src/retry-later.ts then changes failed-message visibility to 10, 40 or 120 seconds, based on the receive count. This explicit backoff replaces the ordinary 60-second wait for reported failures; a crash that cannot report failures falls back to queue visibility.

apps/worker/src/handlers/booking-confirmed.ts uses booking.confirmed:<bookingId> as its job key. A new SQS message ID cannot make the same booking become a new business job. However, the email is sent before completion is recorded. A crash in between, or concurrent copies, can still produce duplicate email. The code chooses that possibility over recording success before sending and losing an email.

ConnectionWhat allows it
Booking topic → either queueSQS subscription plus that queue’s resource policy: sns.amazonaws.com, sqs:SendMessage, exact topic/account conditions
Lambda poller → jobs queueminitickets-dev-worker-role: sqs:ReceiveMessage, sqs:DeleteMessage, sqs:GetQueueAttributes on this queue
Email handler → failed-message delaySame role: sqs:ChangeMessageVisibility on this queue
Jobs queue → jobs DLQSource redrivePolicy; DLQ redrive allow policy permits this source queue only

The API role publishes to SNS; it has no direct queue send permission. The worker does not need send permission on its DLQ: SQS moves failed messages. The policy details live in infra/src/jobs/worker-iam.ts and infra/src/jobs/booking-events.ts.

flowchart TB
F["Worker timeout or failed record"]:::failure --> V["Visibility expires"]:::aws
V --> R["Receive again"]:::service
R -->|"exceeds maxReceiveCount 3"| D["minitickets-dev-jobs-dlq"]:::failure
D -->|"visible count above zero"| A["jobs-dlq-not-empty alarm"]:::aws
A -->|"CloudWatch allowed sns:Publish"| S["minitickets-dev-alerts → email"]:::aws

Legend: red = failure; highlighted = queue retry; AWS = managed recovery and notification services.

What you seeWhyFix
Oldest-message age risesDisabled mapping, exhausted concurrency or slower consumers than producersCheck mapping state, Lambda throttles and downstream latency before increasing concurrency
Same booking email arrives twiceVisibility expired or worker finished sending before recording/deletingFit batch work inside timeout; strengthen business idempotency; do not promise exactly once
message body is not valid JSON or invalid message, wrong fieldsA permanently invalid payload cannot become valid through retryCorrect the producer or compatible schema, inspect metadata and redrive after repair
Whole good batches repeatHandler throws/times out, or partial reporting is misconfiguredCheck mapping response types and invocation logs; shorten the batch if needed
AccessDenied or no receivesWorker queue permissions, queue policy or customer-managed KMS permissions are wrongInspect the exact identity, resource and action; our queues use SSE-SQS

Our jobs DLQ alarm checks visible messages above zero over a 60-second period; notification is not immediate. Partial failures can leave Lambda’s Errors at zero because the invocation returned normally. Watch queue age, DLQ count and per-record logs too. The current alarms cover the email queue; equivalent ticket-path alarms would be additional work.

Redrive means moving DLQ messages back for processing. Repair first, then replay at a rate downstream services can handle. For standard queues, DLQ expiry still uses the original enqueue timestamp; arrival does not grant a fresh retention period. That is why our DLQ retention exceeds the source queue’s. AWS DLQ retention.

At ten times traffic, the email service’s permitted send rate or our five-invocation cap may fail before SQS storage does. More concurrency can increase throttles and repeated attempts. Measure arrival rate, successful completion rate and oldest-message age. If arrivals exceed completions, backlog keeps growing; a queue delays overload rather than eliminating it.

At one hundred times traffic, estimate drain time after a burst. As an example, 6,000 waiting jobs and spare capacity of 20 successful jobs per second need about five minutes, ignoring retries. This is arithmetic, not a MiniTickets benchmark. Add bounded concurrency, downstream rate control and retry jitter, a random variation that prevents simultaneous retries. Our current backoff has no jitter.

In-flight limits matter when many workers hold messages without deleting them. Very long visibility can also delay recovery. For FIFO, one popular message group can serialize most work; choose groups by the smallest business entity that actually requires ordering. AWS recommends many distinct group IDs to spread partition load. AWS FIFO partition guidance.

  • “Receive removes the message.” Receive hides it; delete acknowledges completed processing. Retention can independently expire it.
  • “FIFO means a payment happens exactly once.” FIFO suppresses duplicate sends within its deduplication window and orders groups. External side effects still require idempotency.
  • “A batch is always ten records.” An SQS receive returns up to ten. Lambda can accumulate a larger standard-queue batch, subject to configuration and payload limits.
  • “A DLQ fixes failed work.” It stores exhausted work. Somebody still needs detection, diagnosis, repair and controlled replay.

How would you preserve payment-update order while letting unrelated bookings progress?

Use a FIFO queue with MessageGroupId = bookingId. Give each logical payment event a stable deduplication ID, and make the payment operation idempotent using the provider’s operation key. Process each booking’s events in order while different bookings run concurrently. Validate expected state transitions rather than trusting arrival order alone. This is designed, not built; MiniTickets currently has no payment workflow.

flowchart TB
P["Payment events (designed, not built)"]:::app -.->|"send: sender role sqs:SendMessage; group = bookingId"| Q["Payment FIFO (designed, not built)"]:::service
Q -.->|"ordered: mapping + worker sqs:ReceiveMessage"| W["Payment worker (designed, not built)"]:::app
W -.->|"idempotency key; provider authorization"| A["Payment API (designed, not built)"]:::aws
class P,Q,W,A designed

Legend: dashed boxes and arrows = proposed architecture, not current MiniTickets resources.

Decide what a poison event means for that booking’s later events. Moving it to a DLQ permits later work to proceed and can break the required business sequence. If later actions depend on it, pause or reconcile the booking explicitly rather than treating DLQ removal as permission to continue. AWS DLQ ordering warning.

Why not use one queue for email and tickets?
Consumers of one queue compete for messages. Separate subscribed queues give each workflow its own copy, retry policy and processing speed.
What if processing takes longer than expected?
Bound work to the invocation timeout, reduce batch size or move long work to another execution model. A custom consumer can extend visibility, within the service limit.
Can a long poll return no messages?
Yes. Its wait expires if nothing becomes available. In-flight limits can also prevent further receives, so an empty response alone does not prove there is no work.
Why use the booking ID rather than SQS message ID?
A producer can publish the same business event again with a different transport ID. The booking key identifies the intended operation across those copies.
Should maxReceiveCount always be three?
No. Three is our configuration. Choose enough attempts for realistic transient outages, balanced against delay and repeated side effects; then alarm and retain exhausted work.
  • Receive hides; delete completes; retention eventually expires stored work.
  • Visibility for Lambda should cover six function timeouts plus the batching window.
  • Partial responses require both the mapping setting and correct failed-message IDs.
  • FIFO orders one message group; idempotency protects the business action.
  • Fix the cause before redriving a DLQ, and monitor age as well as count.
Advanced: IAM, config, CLI and code

In the console, open SQS → Queues → minitickets-dev-jobs → Configuration / Dead-letter queue / Monitoring. Then inspect Lambda → minitickets-dev-worker → Configuration → Triggers and the mapping’s batch response setting. Compare source and DLQ retention rather than assuming the DLQ clock restarts.

Read-only queue configuration, using a queue URL already supplied by your environment:

Terminal window
aws sqs get-queue-attributes --queue-url "$JOBS_QUEUE_URL" --attribute-names VisibilityTimeout MessageRetentionPeriod ReceiveMessageWaitTimeSeconds RedrivePolicy
aws lambda list-event-source-mappings --function-name minitickets-dev-worker

Do not poll a production queue just to inspect configuration: receiving changes visibility and receive count. Use queue attributes, CloudWatch and safely filtered application logs first. SQS metrics are approximate, so a single count is not an exact audit of customer outcomes.

For further reading, use the AWS visibility guide, DLQ configuration and the shorter MiniTickets queue chapter.

Answer these without scrolling up. If one feels hard, reread that section.

  1. What changes when a message is received, and what does not?
  2. How do partial batch responses prevent successful records from repeating?
  3. Why do our 10-second and 30-second functions use different queue visibility values?
  4. Which duplicate-email failure window remains in our current handler?
  5. What must be repaired before DLQ redrive, and which metrics confirm recovery?

Redraw it from memory: the jobs queue, Lambda poller, worker and DLQ, labeling receive, delete and retry

Try this: compare VisibilityTimeout and RedrivePolicy in the jobs queue's read-only attributes with createJobQueues

Read this book online: https://book.theversehub.in/