Send and Receive SQS Messages With AWS SDK v3 in TypeScript

Cardboard parcels moving along a conveyor belt in a sorting warehouse

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?

  1. SendThe producer calls SendMessage or SendMessageBatch. SQS stores the message and returns a MessageId. 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).
  2. ReceiveA consumer calls ReceiveMessage and gets up to 10 messages, each with a ReceiptHandle.
  3. HideReceived messages become invisible to other consumers for the visibility timeout: the queue default is 30 seconds, the maximum 12 hours.
  4. DeleteAfter successful processing, the consumer calls DeleteMessage with the receipt handle. SQS never deletes a message just because it was received.
  5. 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; GetQueueUrl finds 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

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

// 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: DelaySeconds from 0 to 900 hides a message for up to 15 minutes after sending (standard queues only).
  • Batch results: SendMessageBatch returns HTTP 200 even when some entries fail. Always read Failed. Entries with SenderFault: true are 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

// 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. MaxNumberOfMessages defaults to 1. Receiving 10 and deleting with DeleteMessageBatch cuts 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. ApproximateReceiveCount shows how many times that has happened.
  • Heartbeat for slow work. ChangeMessageVisibility every 30 seconds keeps a message hidden while it’s still being processed, so a second worker doesn’t pick it up.
  • Clean shutdown. SIGINT or SIGTERM aborts the pending long poll through abortSignal. Any messages SQS handed out in that instant simply reappear after the visibility timeout.

What changes for FIFO queues?

send-fifo.ts

// 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 "*".

sqs-producer-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "SendToOrdersQueue",
      "Effect": "Allow",
      "Action": ["sqs:SendMessage", "sqs:GetQueueUrl"],
      "Resource": "arn:aws:sqs:us-east-1:123456789012:orders"
    }
  ]
}
sqs-consumer-policy.json

{
  "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