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
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
| Standard | Standard + fair queues | FIFO (.fifo) | |
|---|---|---|---|
| Ordering | Best effort | None | Strict within a MessageGroupId |
| Duplicates | Possible (at least once) | Possible | Not added for retried sends within the 5-minute deduplication interval |
MessageGroupId | Optional | Tenant ID: SQS favours quiet tenants when one floods the queue | Required; sends without it fail |
| Throughput | Quota-based (Service Quotas) | Fair queues add no throughput limits | Quota-based; "high throughput" mode: DeduplicationScope=messageGroup + FifoThroughputLimit=perMessageGroupId |
| Use for | Most work queues | Multi-tenant queues with noisy neighbours | Ordered workflows per entity (per order, per account) |
Limits worth knowing
| Item | Value |
|---|---|
| Message size | 1 byte – 1 MiB (larger: S3 + reference, e.g. the Extended Client Library for Java/Python, up to 2 GB) |
| Retention | Default 4 days; 60 s – 14 days |
| Visibility timeout | Default 30 s; 0 s – 12 h |
| Delay | 0 – 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: truemeans the entry itself is bad (size, characters), so don't retry it unchanged. - FIFO: with
ContentBasedDeduplicationon, the deduplication ID is a SHA-256 of the body, not the attributes. Otherwise sendMessageDeduplicationId. 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, onChangeMessageVisibility,MessageNotInflight(the message isn't in flight, for example because its visibility already expired: the hub's reading). Seereference/sqs/errors.md. - Short polling (
WaitTimeSeconds0, the default unless the queue attributeReceiveMessageWaitTimeSecondsis 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").
maxReceiveCountdefaults 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.
RedriveAllowPolicyon the DLQ:allowAll(default),denyAll, orbyQueuewith up to 10sourceQueueArns.- 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.