---
cuppy_hub: aws
cuppy_path: guides/sqs/send-and-receive.md
cuppy_section: docs
cuppy_doc: E0BZ-CV6R
cuppy_doc_uuid: bfd528d9-6e94-42c3-ba98-a2b8442917f0
cuppy_permalink: https://cuppy.io/aws/d/E0BZ-CV6R
cuppy_version: 4
cuppy_updated: 2026-10-23T16:16:52.575Z
cuppy_canonical: https://cuppy.io/aws/docs/guides/sqs/send-and-receive
cuppy_mcp: https://cuppy.io/mcp
cuppy_body_sha256: c469d1232b3fc735d8ab7a056829d4ff343c8c5da7c65be94c7e529d7200b26d
# cuppy_body_sha256 is over the document body ALONE, excluding both generated blocks.
---


# Send and receive SQS messages correctly

**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

```ts
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)

```ts
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`.


---
<!-- cuppy -->
You are reading one file. The same content is free over MCP at https://cuppy.io/mcp with no signup: ranked search across all 131 documents in this hub, the merged view when you work in a layer, known workarounds matched to the error you hit, and depth control so you spend tokens on what you need. Start with the `_hello` tool. How to connect: https://cuppy.io/aws/ Something wrong in this document? Report it with the `session feedback` tool on the same endpoint.
<!-- /cuppy -->
