Lambda Destinations: Async Failure Routing Beyond Plain SQS DLQs

Lambda Destinations: Async Failure Routing Beyond Plain SQS DLQs
7 views

This guide covers Lambda destinations with production-focused examples and trade-offs.

A classic DLQ stores the original event — useful, but often not enough to replay safely. On-failure destinations add invoke context (request id, function arn, approximate timestamp, response payload) so remediator tools can route, dedupe, and replay without archaeology.
⚡ TL;DR: For async invokes, configure OnFailure to SQS or EventBridge with a versioned envelope schema; build a remediator that validates, dedupes by idempotency key, and re-invokes deliberately; keep DLQs as backup for stream sources. Pair with Lambda Recursive Loop Detection and Lambda Timeouts & DLQs.

Destinations vs DLQ — Lambda destinations

Source Failure sink Notes
Async InvocationType=Event On-failure destination Rich context
S3/SNS/EventBridge async On-failure destination Prefer over only CloudWatch Logs
SQS/Kinesis/DDB streams ESM partial failure + DLQ Destinations do not replace stream DLQs
typescript
// cdk/destinations.ts
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as destinations from "aws-cdk-lib/aws-lambda-destinations";
import * as sqs from "aws-cdk-lib/aws-sqs";
import * as events from "aws-cdk-lib/aws-events";

const failQ = new sqs.Queue(this, "FnOnFailure", {
  retentionPeriod: Duration.days(14),
});
const bus = events.EventBus.fromEventBusName(this, "Bus", "platform");

const fn = new lambda.Function(this, "AsyncWorker", {
  runtime: lambda.Runtime.NODEJS_20_X,
  handler: "index.handler",
  code: lambda.Code.fromAsset("dist"),
  retryAttempts: 2,
  onFailure: new destinations.SqsDestination(failQ),
  // alternatively: new destinations.EventBridgeDestination(bus)
});

Envelope schema for replay

Normalize destination payloads into your own schema the moment they land (EventBridge rule → remediator, or SQS consumer).
typescript
// types/failureEnvelope.ts
export type FailureEnvelopeV1 = {
  schema: "lambda.failure/v1";
  functionArn: string;
  requestId: string;
  timestamp: string;
  idempotencyKey: string; // from original event or hash
  payload: unknown;       // original async event
  error?: { type: string; message: string };
};

export function fromDestinationRecord(rec: any): FailureEnvelopeV1 {
  const req = rec.requestContext ?? rec.requestPayload ?? {};
  const payload = rec.requestPayload ?? rec;
  const key =
    payload?.idempotencyKey ??
    hashStable(payload);
  return {
    schema: "lambda.failure/v1",
    functionArn: rec.functionArn ?? req.functionArn,
    requestId: rec.requestContext?.requestId ?? "unknown",
    timestamp: new Date().toISOString(),
    idempotencyKey: key,
    payload,
    error: rec.responsePayload?.errorMessage
      ? { type: rec.responsePayload.errorType, message: rec.responsePayload.errorMessage }
      : undefined,
  };
}

Remediator pattern

typescript
// remediator.ts
export async function replay(env: FailureEnvelopeV1, mode: "dry-run" | "live") {
  if (await alreadySucceeded(env.idempotencyKey)) return { status: "skip" };
  if (mode === "dry-run") return { status: "would_replay", env };
  // ✅ Invoke updated alias deliberately — not the broken $LATEST by accident
  await lambda.send(new InvokeCommand({
    FunctionName: `${env.functionArn}:live`,
    InvocationType: "RequestResponse",
    Payload: Buffer.from(JSON.stringify(env.payload)),
  }));
  await markSucceeded(env.idempotencyKey);
  return { status: "replayed" };
}
❌ Auto-replaying every on-failure message into the same function version that just failed — without a circuit breaker.

Closing checklist

✅ Dos
– ✅ Use on-failure destinations for async invokes
– ✅ Version an envelope schema; index by idempotency key
– ✅ Separate remediator function + alias targeting
– ✅ Retain failure queues ≥ 14 days for incident windows
– ✅ Alarm on destination queue depth ❌ Don’ts
– ❌ Don’t confuse stream source DLQs with async destinations
– ❌ Don’t unbounded auto-replay
– ❌ Don’t drop requestId — you need it for X-Ray/logs
– ❌ Don’t publish failures back onto the originating bus without hop limits

Related reading

Last updated on September 11, 2026

Related: Lambda SnapStart for Python: Pitfalls Beyond the Java Marketing · SQS FIFO + Lambda: Ordered Agent Job Queues Without Double-Applies · Lambda SQS Partial Batch Failures: Poison Messages Without Infinite Redrive

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a comment

No account needed. Name and email are optional.