Updated Nov 5, 20266 min read

AWS SQS with Lambda in 2026: Event Source Mapping, Retries and DLQs

How Lambda polls Amazon SQS, how batch size and partial batch failure work, how visibility timeouts and dead-letter queues interact, the IAM you need, and how to get the same queue pattern from TypeScript without managing the event source mapping.

Amazon SQS and AWS Lambda are the default pairing on AWS for asynchronous work: put a message on a queue, let Lambda poll it, run a function per batch. The SDK call that sends the message is short, and the behaviour that matters lives in the event source mapping: batch size, visibility timeout, partial batch failure, dead-letter queues and the IAM role that lets Lambda read and delete messages.

This guide walks through that mapping end to end, then shows how Encore (infrastructure from code) gives TypeScript services the same SNS and SQS shape from declarations in code, with handlers that must still be idempotent.

SQS and Lambda at a glance

ConceptWhat to know
TriggerEvent source mapping (Lambda polls SQS)
Default batch sizeUp to 10 messages per invoke
Batch windowBuffer up to 5 minutes before invoking
On successLambda deletes the batch from the queue
On failure (default)Whole batch becomes visible again after the visibility timeout
Partial failureEnable ReportBatchItemFailures, return failed messageIds
Poison messagesRedrive policy to a dead-letter queue (maxReceiveCount)
DeliveryAt least once: handlers must be idempotent
RegionFunction and queue in the same Region

How the event source mapping works

An SQS event source mapping is a Lambda-managed poller. Lambda receives a batch, hides those messages for the queue's visibility timeout (30 seconds by default, up to 12 hours), and invokes your function synchronously with an SQSEvent. If the invocation succeeds, Lambda deletes the messages. If it fails and you have not enabled partial batch responses, every message in the batch becomes visible again when the timeout expires and is retried.

Set the visibility timeout above the slowest expected batch, or the same message can be processed by two concurrent invocations. For long handlers, extend visibility with ChangeMessageVisibility or raise the queue default. Standard and FIFO queues are both supported; FIFO records include MessageGroupId and MessageDeduplicationId.

By default Lambda takes up to 10 messages per batch. Raising the batch size and adding a batch window (up to 5 minutes) cuts invoke count for steady streams. Low-traffic queues can still wait up to about 20 seconds before an invoke even with a shorter window.

Handler with partial batch failure

Without partial batch reporting, one bad message requeues the successes too. Enable ReportBatchItemFailures on the mapping and return only the failures:

// handler.ts import type { SQSEvent, SQSBatchResponse, SQSBatchItemFailure, SQSRecord, Context, } from "aws-lambda"; export async function handler( event: SQSEvent, _context: Context, ): Promise<SQSBatchResponse> { const batchItemFailures: SQSBatchItemFailure[] = []; for (const record of event.Records) { try { await processMessage(record); } catch (err) { console.error("failed", record.messageId, err); batchItemFailures.push({ itemIdentifier: record.messageId }); } } return { batchItemFailures }; } async function processMessage(record: SQSRecord): Promise<void> { const body = JSON.parse(record.body) as { orderId: string }; // Make this safe to run twice for the same orderId. console.log("processing", body.orderId); }

Install @types/aws-lambda for the event types. On FIFO queues, stop at the first failure and return that message plus every later unprocessed one in the batch so order inside the message group is preserved. Throwing outside the per-record try/catch still fails the whole batch.

Retries, DLQs and IAM

Retries are the visibility timeout loop: undeleted messages reappear and count toward ApproximateReceiveCount. Attach a dead-letter queue with a redrive policy and a maxReceiveCount (for example 5) so poison messages leave the main queue instead of burning concurrency. Alarm on DLQ depth in CloudWatch, then use SQS redrive after you fix the handler.

The function's execution role needs sqs:ReceiveMessage, sqs:DeleteMessage and sqs:GetQueueAttributes on the source queue, plus sqs:SendMessage on the DLQ if Lambda (or the redrive) writes there. Keep the role scoped to those queue ARNs. Producers need sqs:SendMessage on the source queue only.

What you still own with the raw pattern

Beyond the handler you maintain the queue and DLQ in every environment (Terraform, CDK or console), the event source mapping settings, the IAM role, the queue URL or ARN in config, and a local substitute such as LocalStack or a manual send into a shared queue. When one event must reach several consumers, an SNS topic fans out to one queue per consumer, each with its own mapping.

SQS-backed Pub/Sub from TypeScript with Encore

Encore

Encore combines an open source infrastructure SDK for TypeScript and Go with a platform that provisions and deploys into your own AWS or GCP account. A Pub/Sub topic and its subscriptions are declared in TypeScript. On AWS Encore provisions an SNS topic with an SQS queue and a dead-letter queue per subscription, as described in the AWS infrastructure docs, and runs each subscription handler for you. There is no event source mapping or polling loop in your code. Services run on containers such as Fargate, not on Lambda.

The Infrastructure page in the Encore dashboard with a database's configuration panel open, showing fields for vCPUs, memory, storage, availability type, max connections and backups.
Each provisioned resource can be reviewed and its configuration edited per environment in the Encore dashboard. The screenshot shows a GCP Cloud SQL database.

Set up the app

Get started
brew install encoredev/tap/encore &&
encore app create
Copy

Run in your terminal to get started locally.

~
$ encore app create shop --example=ts/empty$ cd shop && mkdir orders
// orders/encore.service.ts import { Service } from "encore.dev/service"; export default new Service("orders");
// orders/orders.ts import { api } from "encore.dev/api"; import { Topic, Subscription } from "encore.dev/pubsub"; interface OrderPlaced { orderId: string; } export const orderPlaced = new Topic<OrderPlaced>("order-placed", { deliveryGuarantee: "at-least-once", }); export const placeOrder = api( { expose: true, method: "POST", path: "/orders" }, async ({ orderId }: { orderId: string }): Promise<{ ok: boolean }> => { await orderPlaced.publish({ orderId }); return { ok: true }; }, ); // Throwing retries per the subscription RetryPolicy, then the DLQ. const _ = new Subscription(orderPlaced, "send-confirmation", { handler: async (event) => { // Idempotent: safe if the same orderId is delivered twice. console.log("sending confirmation for", event.orderId); }, retryPolicy: { maxRetries: 5, minBackoff: "1s" }, });

encore run uses an in-memory Pub/Sub, so a POST to /orders reaches the subscription locally without AWS credentials. The local development dashboard shows the publish and the handler. A second consumer is another new Subscription on the same topic. Delivery is still at least once, so handlers must be idempotent the same way as on a raw SQS Lambda. Encore provisions an IAM role for the service, so queue URLs and access keys do not sit in config. Deploying to your account is covered in deploying to your own cloud.

An AI coding agent adding a worker writes a subscription next to the API in the same change and tests it locally.

Best fit: TypeScript teams on AWS who want SQS-backed queues with dead-letter handling and local delivery, declared next to the API.

Good to know

  • Match visibility timeout to batch runtime: too low causes duplicate processing, too high delays retries.
  • Monitor ApproximateAgeOfOldestMessage and DLQ depth; a silent poison message looks like a stuck queue.
  • FIFO with Lambda preserves order per message group, at lower throughput than a standard queue.

Frequently asked questions

How does Lambda consume messages from SQS?

You create an event source mapping between the queue and the function. Lambda long-polls the queue, invokes the function synchronously with a batch of messages, and deletes the batch when the invocation succeeds. The function and the queue must be in the same AWS Region.

What is ReportBatchItemFailures for SQS Lambda?

A function response type that lets you return only the message IDs that failed. Without it, one failure in a batch makes the whole batch visible again after the visibility timeout. With it, Lambda deletes the successes and retries only the failures.

Do SQS-triggered Lambda handlers need to be idempotent?

Yes. Event source mappings deliver at least once, so the same message can arrive more than once. Record processed IDs in a database, or make the side effect safe to repeat, before acknowledging success.

How do I get SQS-backed workers from TypeScript without an event source mapping?

With Encore, declare a Topic and a Subscription with a handler in TypeScript. Messages are delivered in memory during local development, and on AWS Encore provisions an SNS topic with an SQS queue and dead-letter queue per subscription and runs the handler for you.

Deploy to your cloud on AWS and Google Cloud
without managing infrastructure

Encore lets developers and agents define application infrastructure directly in code, then automatically provisions it from local development to production in your AWS or GCP account.

~/orders — claude
Claude Code
Claude Code v2.1.180Opus 5.5~/orders