Photo by Hyundai Motor Group on Unsplash
To send an SQS message with AWS SDK v3, create an SQSClient from @aws-sdk/client-sqs and call client.send(new SendMessageCommand({ QueueUrl, MessageBody })). To receive, call ReceiveMessageCommand with WaitTimeSeconds: 20 for long polling, process each message, then delete it with its ReceiptHandle. Undeleted messages reappear after the visibility timeout.
This guide is for TypeScript developers wiring Amazon SQS into a Node.js service: a producer that enqueues jobs and a worker that processes them. It covers the SQS send message flow with AWS SDK v3 from the first call to a production-shaped consumer loop, including the parts that cause duplicate or lost work: batch failures, visibility timeouts, deletes and FIFO ordering. You’ll end with four small files you can copy, and the IAM policy each side needs.
If your queue code still uses aws-sdk v2 (new AWS.SQS().sendMessage(params).promise()), the steps to migrate a Node.js app from AWS SDK v2 to v3 come first, and the AWS SDK v2 to v3 code converter translates single files.
How does an SQS message move through a queue?
- SendThe producer calls
SendMessageorSendMessageBatch. SQS stores the message and returns aMessageId. When several queues need the same event, publish it once to an SNS topic they subscribe to, as in the guide to publish an SNS message with AWS SDK v3 in TypeScript. If each queue should get only some of the events, rules can pick them instead, as in the guide to send events to EventBridge with AWS SDK v3 (PutEvents). - ReceiveA consumer calls
ReceiveMessageand gets up to 10 messages, each with aReceiptHandle. - HideReceived messages become invisible to other consumers for the visibility timeout: the queue default is 30 seconds, the maximum 12 hours.
- DeleteAfter successful processing, the consumer calls
DeleteMessagewith the receipt handle. SQS never deletes a message just because it was received. - Or reappearIf the consumer crashes or doesn’t delete in time, the message becomes visible again and another consumer receives it. With a redrive policy, repeated failures move it to a dead-letter queue. The script to find SQS queues without a dead-letter queue checks which of yours have one.
That means delivery is at least once: a standard queue can deliver a message more than once, so your handler must be idempotent. FIFO queues add ordering and deduplication, covered below.
Prerequisites
- Node.js 20 or later, and
@aws-sdk/client-sqs(npm install @aws-sdk/client-sqs). - An existing queue and its URL, for example
https://sqs.us-east-1.amazonaws.com/123456789012/orders. The URL is what every call takes;GetQueueUrlfinds it from the name. - Credentials for a profile or role with the permissions below, and the queue’s Region.
Create one SQS client with the right timeout
// sqs-client.ts
import { SQSClient } from "@aws-sdk/client-sqs";
export const QUEUE_URL = process.env.QUEUE_URL ?? "https://sqs.us-east-1.amazonaws.com/123456789012/orders";
// One client per process. requestTimeout must be longer than the 20 s long poll.
export const sqs = new SQSClient({
region: process.env.AWS_REGION ?? "us-east-1",
requestHandler: { connectionTimeout: 3_000, requestTimeout: 30_000, throwOnRequestTimeout: true },
});
The requestTimeout is the one setting people get wrong. A long-poll receive legitimately waits up to 20 seconds, so the HTTP timeout must be longer than WaitTimeSeconds. throwOnRequestTimeout: true makes a real timeout throw instead of only logging a warning. The same settings, plus maxAttempts and retryMode, control how the client retries throttled or failed calls; the guide to configure retries and timeouts in AWS SDK for JavaScript v3 explains each one. The client also verifies the MD5 digests SQS returns for message bodies by default, so corrupted payloads fail loudly.
How to send an SQS message with AWS SDK v3
// send.ts
import { SendMessageCommand, SendMessageBatchCommand, type SendMessageBatchRequestEntry } from "@aws-sdk/client-sqs";
import { randomUUID } from "node:crypto";
import { sqs, QUEUE_URL } from "./sqs-client.js";
type OrderEvent = { orderId: string; status: "created" | "paid" | "shipped" };
export async function sendOne(event: OrderEvent): Promise<string | undefined> {
const res = await sqs.send(
new SendMessageCommand({
QueueUrl: QUEUE_URL,
MessageBody: JSON.stringify(event),
MessageAttributes: {
eventType: { DataType: "String", StringValue: event.status },
},
}),
);
return res.MessageId;
}
// SendMessageBatch takes at most 10 entries, and a 200 response can still
// contain failed entries, so check Failed and resend only those.
export async function sendMany(events: OrderEvent[]): Promise<void> {
for (let i = 0; i < events.length; i += 10) {
let entries: SendMessageBatchRequestEntry[] = events.slice(i, i + 10).map((e) => ({
Id: randomUUID(),
MessageBody: JSON.stringify(e),
}));
for (let attempt = 1; entries.length > 0; attempt++) {
const res = await sqs.send(new SendMessageBatchCommand({ QueueUrl: QUEUE_URL, Entries: entries }));
const failed = res.Failed ?? [];
const senderFaults = failed.filter((f) => f.SenderFault);
if (senderFaults.length > 0) {
throw new Error(`Rejected entries: ${senderFaults.map((f) => `${f.Id}: ${f.Code}`).join(", ")}`);
}
if (failed.length > 0 && attempt >= 3) throw new Error(`${failed.length} entries still failing`);
const failedIds = new Set(failed.map((f) => f.Id));
entries = entries.filter((e) => failedIds.has(e.Id));
}
}
}
Things to know when sending:
- Size: a message body is 1 byte to 1 MiB (1,048,576 bytes), and a batch of up to 10 messages is limited to 1 MiB in total. For bigger payloads, upload to S3 and send the object key; see how to upload a file to S3 with S3Client in TypeScript.
- Content: XML, JSON and plain text only, within the allowed Unicode ranges. Invalid characters return
InvalidMessageContents. Base64-encode binary data. - Attributes: up to 10 message attributes, useful for routing without parsing the body.
- Delay:
DelaySecondsfrom 0 to 900 hides a message for up to 15 minutes after sending (standard queues only). - Batch results:
SendMessageBatchreturns HTTP 200 even when some entries fail. Always readFailed. Entries withSenderFault: trueare bad requests that will fail again; the others are worth resending, which the code above does up to 3 times.
How do you receive and delete messages?
// consume.ts
import {
ReceiveMessageCommand,
DeleteMessageBatchCommand,
ChangeMessageVisibilityCommand,
type Message,
} from "@aws-sdk/client-sqs";
import { sqs, QUEUE_URL } from "./sqs-client.js";
const VISIBILITY_SECONDS = 60;
const controller = new AbortController();
process.on("SIGINT", () => controller.abort());
process.on("SIGTERM", () => controller.abort());
async function handle(message: Message): Promise<void> {
const event = JSON.parse(message.Body ?? "{}") as { orderId?: string };
console.log(`processing order ${event.orderId} (receive count ${message.Attributes?.ApproximateReceiveCount})`);
// ... your work here. Throwing leaves the message on the queue for a retry.
}
// Extend the visibility timeout while a slow message is still being processed.
function keepInvisible(receiptHandle: string): NodeJS.Timeout {
return setInterval(() => {
sqs
.send(
new ChangeMessageVisibilityCommand({
QueueUrl: QUEUE_URL,
ReceiptHandle: receiptHandle,
VisibilityTimeout: VISIBILITY_SECONDS,
}),
)
.catch((err: unknown) => console.error("heartbeat failed", err));
}, (VISIBILITY_SECONDS / 2) * 1_000);
}
async function poll(): Promise<void> {
while (!controller.signal.aborted) {
let messages: Message[] = [];
try {
const res = await sqs.send(
new ReceiveMessageCommand({
QueueUrl: QUEUE_URL,
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20, // long polling: wait up to 20 s for messages
VisibilityTimeout: VISIBILITY_SECONDS,
MessageSystemAttributeNames: ["ApproximateReceiveCount"],
MessageAttributeNames: ["All"],
}),
{ abortSignal: controller.signal },
);
messages = res.Messages ?? [];
} catch (err) {
if (err instanceof Error && err.name === "AbortError") break;
throw err;
}
const done: { Id: string; ReceiptHandle: string }[] = [];
await Promise.all(
messages.map(async (m, i) => {
if (!m.ReceiptHandle) return;
const heartbeat = keepInvisible(m.ReceiptHandle);
try {
await handle(m);
done.push({ Id: String(i), ReceiptHandle: m.ReceiptHandle });
} catch (err) {
console.error(`message ${m.MessageId} failed; it will be received again`, err);
} finally {
clearInterval(heartbeat);
}
}),
);
// Delete only what succeeded. Failed deletes come back in the response.
if (done.length > 0) {
const res = await sqs.send(new DeleteMessageBatchCommand({ QueueUrl: QUEUE_URL, Entries: done }));
for (const f of res.Failed ?? []) console.error(`delete failed for entry ${f.Id}: ${f.Code}`);
}
}
console.log("consumer stopped");
}
poll().catch((err: unknown) => {
console.error(err);
process.exit(1);
});
How the loop behaves:
- Long polling.
WaitTimeSeconds: 20, the maximum, keeps the connection open until messages arrive or 20 seconds pass. Without it, a receive is a short poll that samples a subset of SQS servers and often returns nothing from a nearly empty queue, costing a request each time. - Up to 10 at a time.
MaxNumberOfMessagesdefaults to 1. Receiving 10 and deleting withDeleteMessageBatchcuts API calls, and so request charges, by up to ten times. - Delete only successes. A message whose handler throws is left alone and comes back after the visibility timeout.
ApproximateReceiveCountshows how many times that has happened. - Heartbeat for slow work.
ChangeMessageVisibilityevery 30 seconds keeps a message hidden while it’s still being processed, so a second worker doesn’t pick it up. - Clean shutdown.
SIGINTorSIGTERMaborts the pending long poll throughabortSignal. Any messages SQS handed out in that instant simply reappear after the visibility timeout.
What changes for FIFO queues?
// send-fifo.ts
import { SendMessageCommand } from "@aws-sdk/client-sqs";
import { sqs } from "./sqs-client.js";
const FIFO_URL = "https://sqs.us-east-1.amazonaws.com/123456789012/orders.fifo";
export async function sendOrdered(orderId: string, step: string, eventId: string): Promise<void> {
await sqs.send(
new SendMessageCommand({
QueueUrl: FIFO_URL,
MessageBody: JSON.stringify({ orderId, step }),
MessageGroupId: orderId, // strict order within one order, parallel across orders
MessageDeduplicationId: eventId, // same ID within 5 minutes = delivered once
}),
);
}
| Standard queue | FIFO queue (.fifo) |
|
|---|---|---|
| Ordering | Best effort | Strict within a MessageGroupId |
| Duplicates | Possible | Deduplicated for 5 minutes by MessageDeduplicationId |
MessageGroupId |
Optional; enables fair queues for multi-tenant load | Required, or the send fails |
Per-message DelaySeconds |
Supported | Not supported; set a delay on the queue |
| Throughput | Nearly unlimited | 300 API calls per second per action per partition without high-throughput mode; batching raises it to 3,000 messages |
If the queue has ContentBasedDeduplication enabled, you can omit MessageDeduplicationId and SQS uses a SHA-256 hash of the body. Choose group IDs carefully: while one message of a group is in flight, no other message from that group is delivered. One group per entity, such as an order ID, keeps order where it matters and parallelism everywhere else. Current limits are listed in the Amazon SQS message quotas.
Permissions needed
Split producer and consumer permissions so a compromised worker can’t flood the queue and a producer can’t delete work. Batch calls use the same actions as their single versions: SendMessageBatch needs sqs:SendMessage, DeleteMessageBatch needs sqs:DeleteMessage. These are identity policies; the queue’s own access policy can grant access too, and the script to find SQS queues and SNS topics open to everyone checks that none allows "*".
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "SendToOrdersQueue",
"Effect": "Allow",
"Action": ["sqs:SendMessage", "sqs:GetQueueUrl"],
"Resource": "arn:aws:sqs:us-east-1:123456789012:orders"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ConsumeOrdersQueue",
"Effect": "Allow",
"Action": [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:ChangeMessageVisibility",
"sqs:GetQueueUrl"
],
"Resource": "arn:aws:sqs:us-east-1:123456789012:orders"
}
]
}
If the queue uses SSE-KMS with a customer managed key, producers also need kms:GenerateDataKey and kms:Decrypt on that key, and consumers need kms:Decrypt. The method to find the IAM actions your AWS SDK for JavaScript code needs covers these mappings, the IAM policy generator for TypeScript code drafts a policy from the files above, and the checklist to review a generated IAM policy for least privilege tightens it.
Troubleshooting common SQS errors
| Error or symptom | Cause and fix |
|---|---|
QueueDoesNotExist |
Wrong URL, wrong Region in the client, or a deleted queue. Queue names and URLs are case-sensitive. |
AccessDenied / KmsAccessDenied |
Missing sqs: or kms: permission, or a queue policy denying the caller. Work through how to troubleshoot AWS IAM access denied errors. |
| Every send to a FIFO queue fails | No MessageGroupId, or no MessageDeduplicationId on a queue without content-based deduplication. Both are required there. |
TimeoutError on every receive |
requestTimeout shorter than WaitTimeSeconds. Make it at least a few seconds longer. |
| Messages processed twice | Processing took longer than the visibility timeout, or a delete failed. Raise the timeout, add the heartbeat, and make the handler idempotent. |
| A deleted message comes back | Each receive returns a new receipt handle, and a delete with an older one might not delete the message. Always delete with the handle from the most recent receive. |
OverLimit on receive |
Too many in-flight messages (about 120,000 for a standard queue). Delete processed messages promptly. |
Limits of this approach
- A hand-written poll loop is right for containers and long-running workers. In Lambda, use an SQS event source mapping instead of polling yourself; your handler receives the batch and Lambda deletes messages on success. The guide to investigate Lambda errors with CloudWatch helps when those invocations fail.
- SQS doesn’t guarantee exactly-once processing on standard queues. Idempotency is your job.
- Payloads over 1 MiB need S3 plus a pointer. AWS’s extended client libraries for this exist for Java and Python, not JavaScript.
- Messages expire after the queue’s retention period: 4 days by default, 14 days at most.
For more v3 patterns, see the AWS SDK v3 examples in TypeScript, including how to invoke a Lambda function with AWS SDK v3. Porting a Python worker? The guide to port a boto3 script to Node.js with SDK v3 and the boto3 to AWS SDK v3 converter cover the translation. To check queue depth or find which queues exist without writing code, you can list AWS resources with natural language from your terminal using ChatWithCloud and a read-only profile.
Frequently asked questions
How do I send a message to SQS with AWS SDK v3?
Install @aws-sdk/client-sqs, create an SQSClient, and call send(new SendMessageCommand({ QueueUrl, MessageBody })). The response contains the MessageId.
Why does ReceiveMessage return no messages when the queue isn’t empty?
Without WaitTimeSeconds, receives are short polls that sample only some servers. Set WaitTimeSeconds to 20. Messages may also be in flight with another consumer.
Does SQS delete a message after I receive it?
No. You must call DeleteMessage or DeleteMessageBatch with the receipt handle, or the message reappears after the visibility timeout.
How many messages can I send in one batch?
Up to 10 entries per SendMessageBatch, with a combined size of at most 1 MiB. Check the Failed list in every response.
Related guides
Ask your AWS account in plain English
Your first 15 runs are free, with no OpenAI key needed.
npx chatwithcloud