Send and receive SQS messages correctly

Produce and consume Amazon SQS messages from TypeScript without losing or duplicating work — standard vs FIFO vs fair queues, SendMessageBatch partial failures, FIFO MessageGroupId and deduplication (5-minute window, content-based), long polling with WaitTimeSeconds 20, deleting by receipt handle, extending visibility for slow work, dead-letter queues with redrive and redrive-allow policies, and the 1 MiB message limit.

markdownguidev4E0BZ-CV6Rstage: buildtrust: officialservice: sqsinterface: sdkSQS SendMessageSendMessageBatch FailedReceiveMessage long pollingWaitTimeSeconds 20empty receive short pollingDeleteMessage receipt handleChangeMessageVisibilityvisibility timeoutFIFO queue MessageGroupIdMessageDeduplicationIdcontent-based deduplication5 minute deduplication intervalFIFO group blockedhigh throughput FIFOfair queues noisy neighbordead-letter queueRedrivePolicy maxReceiveCount default 10RedriveAllowPolicyDLQ redriveSQS message size 1 MiBmessage retention 14 daysdelay queue

Created 18 Oct 2026 · Modified 23 Oct 2026 · 4 versions

Assume every message can arrive twice and late. Delete a message only after its work is done, long-poll (20 s) instead of spinning on empty receives, give every queue a dead-letter queue, and check the Failed list of every batch send.

Pick the queue type

StandardStandard + fair queuesFIFO (.fifo)
OrderingBest effortNoneStrict within a MessageGroupId
DuplicatesPossible (at least once)PossibleNot added for retried sends within the 5-minute deduplication interval
MessageGroupIdOptionalTenant ID: SQS favours quiet tenants when one floods the queueRequired; sends without it fail
ThroughputQuota-based (Service Quotas)Fair queues add no throughput limitsQuota-based; "high throughput" mode: DeduplicationScope=messageGroup + FifoThroughputLimit=perMessageGroupId
Use forMost work queuesMulti-tenant queues with noisy neighboursOrdered workflows per entity (per order, per account)

Limits worth knowing

ItemValue
Message size1 byte – 1 MiB (larger: S3 + reference, e.g. the Extended Client Library for Java/Python, up to 2 GB)
RetentionDefault 4 days; 60 s – 14 days
Visibility timeoutDefault 30 s; 0 s – 12 h
Delay0 – 15 min
Batch≤ 10 entries (SendMessageBatch, DeleteMessageBatch); MaxNumberOfMessages 1–10 per receive
Message attributes≤ 10
Characters#x9, #xA, #xD, #x20–#xD7FF, #xE000–#xFFFD, #x10000–#x10FFFF; anything else → InvalidMessageContents

Producer

import { SQSClient, SendMessageBatchCommand, type SendMessageBatchRequestEntry } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({});

/** Sends up to 10 at a time and retries only the entries SQS reports as failed. */
export async function sendAll(queueUrl: string, bodies: object[], groupId?: string) {
  for (let i = 0; i < bodies.length; i += 10) {
    let entries: SendMessageBatchRequestEntry[] = bodies.slice(i, i + 10).map((b, j) => ({
      Id: `m${i + j}`,                       // ≤80 chars: letters, digits, - and _
      MessageBody: JSON.stringify(b),
      ...(groupId ? { MessageGroupId: groupId } : {}), // FIFO: required; standard: fair-queue tenant
    }));
    for (let attempt = 0; entries.length > 0 && attempt < 3; attempt++) {
      const res = await sqs.send(new SendMessageBatchCommand({ QueueUrl: queueUrl, Entries: entries }));
      const failed = new Set((res.Failed ?? []).filter((f) => !f.SenderFault).map((f) => f.Id));
      const fatal = (res.Failed ?? []).filter((f) => f.SenderFault);
      if (fatal.length) throw new Error(`Rejected: ${fatal.map((f) => `${f.Id} ${f.Code}`).join(", ")}`);
      entries = entries.filter((e) => failed.has(e.Id));
    }
    if (entries.length) throw new Error(`Gave up on ${entries.length} messages`);
  }
}
  • A batch call can succeed while some entries fail. Always read Failed. SenderFault: true means the entry itself is bad (size, characters), so don't retry it unchanged.
  • FIFO: with ContentBasedDeduplication on, the deduplication ID is a SHA-256 of the body, not the attributes. Otherwise send MessageDeduplicationId. Retrying with the same ID within 5 minutes is safe.

Consumer (outside Lambda)

import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand, ChangeMessageVisibilityCommand } from "@aws-sdk/client-sqs";

const sqs = new SQSClient({});

export async function pollForever(queueUrl: string, work: (body: string) => Promise<void>) {
  for (;;) {
    const { Messages = [] } = await sqs.send(new ReceiveMessageCommand({
      QueueUrl: queueUrl,
      MaxNumberOfMessages: 10,
      WaitTimeSeconds: 20,              // long polling: fewer empty and false-empty responses
      VisibilityTimeout: 60,            // longer than the work takes
      MessageSystemAttributeNames: ["ApproximateReceiveCount"],
    }));
    for (const m of Messages) {
      const keepAlive = setInterval(() => {   // slow job: extend before the timeout runs out
        void sqs.send(new ChangeMessageVisibilityCommand({ QueueUrl: queueUrl, ReceiptHandle: m.ReceiptHandle!, VisibilityTimeout: 60 }));
      }, 45_000);
      try {
        await work(m.Body ?? "");
        await sqs.send(new DeleteMessageCommand({ QueueUrl: queueUrl, ReceiptHandle: m.ReceiptHandle! }));
      } catch (err) {
        console.error("will retry after visibility timeout", m.MessageId, err); // don't delete
      } finally {
        clearInterval(keepAlive);
      }
    }
  }
}
  • Delete with the receipt handle you received with the message you processed. The related errors are ReceiptHandleIsInvalid (the handle isn't valid) and, on ChangeMessageVisibility, MessageNotInflight (the message isn't in flight, for example because its visibility already expired: the hub's reading). See reference/sqs/errors.md.
  • Short polling (WaitTimeSeconds 0, the default unless the queue attribute ReceiveMessageWaitTimeSeconds is set) samples only some servers, so empty responses don't mean an empty queue.
  • FIFO: a received-but-undeleted message blocks its whole group for other consumers until it's deleted or its visibility expires. You can't ask for a specific group.

Dead-letter queue

aws sqs set-queue-attributes --queue-url <source-url> --attributes '{
  "RedrivePolicy": "{\"deadLetterTargetArn\":\"<dlq-arn>\",\"maxReceiveCount\":5}"
}'
  • The DLQ must be in the same account and Region, and must exist first. Standard queues need a standard DLQ and FIFO queues a FIFO DLQ (the hub's reading of the type rule; the page says only "same account and Region").
  • maxReceiveCount defaults to 10. Pick a value that tolerates transient failures; 1 sends a message to the DLQ after one failed receive.
  • Retention: on standard queues the original enqueue time is kept, so set the DLQ's retention longer than the source queue's or messages expire soon after arriving. On FIFO queues the timestamp resets on move.
  • RedriveAllowPolicy on the DLQ: allowAll (default), denyAll, or byQueue with up to 10 sourceQueueArns.
  • When the bug is fixed, redrive moves messages from the DLQ back to the source queue.
  • Avoid a DLQ on FIFO queues whose exact order must be preserved.
  • Alarm on messages arriving in the DLQ (guides/cloudwatch/create-alarms.md).

Encryption and key policies for producers like SNS, EventBridge and S3: reference/sqs/queue-settings-and-encryption.md.

Read this document as markdown — the same bytes an agent receives.