AWS SQS Consumers
Amazon SQS gives managed queues without operating Redis. Node workers use AWS SDK v3 with long polling, visibility timeouts, and dead-letter queues (DLQ).
Search across all documentation pages
Amazon SQS gives managed queues without operating Redis. Node workers use AWS SDK v3 with long polling, visibility timeouts, and dead-letter queues (DLQ).
Quick-reference recipe card - copy-paste ready.
import {
SQSClient,
ReceiveMessageCommand,
DeleteMessageCommand,
} from "@aws-sdk/client-sqs";
const sqs = new SQSClient({});
const queueUrl = process.env.SQS_QUEUE_URL!;
const res = await sqs.send(
new ReceiveMessageCommand({
QueueUrl: queueUrl,
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20,
VisibilityTimeout: 60,
})
);
for (const msg of res.Messages ?? []) {
await processBody(msg.Body!);
await sqs.send(
new DeleteMessageCommand({ QueueUrl: queueUrl, ReceiptHandle: msg.ReceiptHandle! })
);
}When to reach for this:
// src/sqs/poll.ts
import {
SQSClient,
ReceiveMessageCommand,
DeleteMessageCommand,
ChangeMessageVisibilityCommand,
} from "@aws-sdk/client-sqs";
const sqs = new SQSClient({ region: process.env.AWS_REGION });
const queueUrl = process.env.SQS_QUEUE_URL!;
async function handleMessage(body: string) {
const payload = JSON.parse(body) as { type: string; orderId: string };
if (payload.type === "fulfill") {
await fulfillOrder(payload.orderId);
}
}
export async function pollForever(signal: AbortSignal) {
while (!signal.aborted) {
const res = await sqs.send(
new ReceiveMessageCommand({
QueueUrl: queueUrl,
MaxNumberOfMessages: 10,
WaitTimeSeconds: 20,
VisibilityTimeout: 120,
MessageAttributeNames: ["All"],
})
);
for (const msg of res.Messages ?? []) {
try {
await handleMessage(msg.Body ?? "{}");
await sqs.send(
new DeleteMessageCommand({
QueueUrl: queueUrl,
ReceiptHandle: msg.ReceiptHandle!,
})
);
} catch (err) {
console.error({ msg: "sqs_handler_error", messageId: msg.MessageId, err });
// Message returns to queue after visibility timeout
}
}
}
}
// src/worker-main.ts
const ac = new AbortController();
process.on("SIGTERM", () => ac.abort());
await pollForever(ac.signal);What this demonstrates:
WaitTimeSeconds: 20 reduces cost and CPU spinvisibility_timeout >= p99_processing_time * 1.5
ChangeMessageVisibility for variable-length jobs{
"RedrivePolicy": {
"deadLetterTargetArn": "arn:aws:sqs:...:dlq",
"maxReceiveCount": 5
}
}| Type | Ordering | Throughput | Dedup |
|---|---|---|---|
| Standard | Best-effort | Very high | App-level idempotency |
| FIFO | Per message group | 300 TPS/group | Optional content dedup |
MessageGroupId// Message body may be SNS wrapper JSON - unwrap Envelope
const outer = JSON.parse(body);
const inner = outer.Message ? JSON.parse(outer.Message) : outer;maxReceiveCount + DLQ alarm.Message field.| Alternative | Use When | Don't Use When |
|---|---|---|
| BullMQ | Redis already running, rich job API | Want zero queue infra |
| Kinesis | Streaming analytics | Simple task queue |
| EventBridge | Event routing rules | Point-to-point worker queue only |
| Google Pub/Sub | GCP stack | AWS-only org |
sqs-consumer wraps polling with events. Fine for ECS workers. Understand visibility semantics either way.
Scale consumers on ApproximateNumberOfMessagesVisible metric. Avoid duplicate tight loops without long poll.
No. Standard queue is at-least-once. Design idempotent handlers.
Use S3 extended client pattern - SQS carries S3 pointer when >256KB.
sqs:ReceiveMessage, DeleteMessage, ChangeMessageVisibility on queue ARN only.
LocalStack or ElasticMQ for integration tests. ElasticMQ docker for laptop.
SendMessageCommand from API with small JSON body. Same idempotency rules as BullMQ producer.
Default 4 days. Raise to 14 for replay windows during outages.
Batching and multiple message group ids scale parallel ordered streams.
Requests count. Long polling reduces empty receives. Batch receive up to 10 messages.
Stack versions: This page was written for Node.js 24.18.0 (Active LTS), npm 10+, TypeScript 5.6+, Express 5, Fastify 5, and NestJS 11.
Reviewed by Chris St. John·Last updated Jul 19, 2026