Guide · AWS Lambda

MCP Server Lambda Destinations — async invocation result routing, OnSuccess, OnFailure

Lambda Destinations route the result of an async invocation to a downstream target after the function finishes — whether it succeeded or failed. Unlike synchronous invocations (where the caller waits and receives the response directly), async invocations (triggered by SNS, EventBridge, S3 event notifications, or the InvocationType: Event SDK flag) fire-and-forget: the caller gets an HTTP 202 immediately and the function runs in a queue. Without Destinations, the caller has no way to receive the function's output or know it failed beyond checking CloudWatch Logs. Destinations solve this: configure an OnSuccess target (receives the function's return value) and/or an OnFailure target (receives the error after all retry attempts are exhausted) — both with a rich envelope containing the original event, the response or error, and invocation metadata.

TL;DR

Lambda Destinations only work with async invocations. Use OnSuccess → SQS to collect probe results for batch processing, OnFailure → SQS DLQ for dead-letter handling with full response context (unlike a bare DLQ), or OnSuccess → EventBridge to fan out results to multiple consumers. Set maximumRetryAttempts: 0 for probe workers where duplicates are harmful. DLQ captures only original event payload; Destinations captures original event + response payload + error details.

Async invocation: when Destinations apply

Destinations only activate on asynchronous invocations. A Lambda function is invoked asynchronously when:

Destinations do not apply to synchronous invocations (API Gateway, Function URLs with direct request/response, ALB, or InvocationType: RequestResponse). For those, the caller always receives the response directly.

Destination targets and their use cases

TargetOnSuccessOnFailureBest for
SQS queueYesYesBatch processing of results; DLQ-style failure collection with full context
SNS topicYesYesFan-out to multiple subscribers; email/Slack notification on failure
EventBridge event busYesYesComplex routing rules; multiple downstream consumers; audit trail
Lambda functionYesYesChaining: next step in a pipeline triggered by success; retry logic in a separate function

For AliveMCP's probe worker architecture, the pattern is: EventBridge rule triggers the probe Lambda asynchronously, OnSuccess → SQS collects probe results (status, latency, schema diff), OnFailure → SQS DLQ captures probe errors for alerting. The probe function itself remains stateless — it does not write to the database; the SQS consumer does.

Payload envelope structure

When a Destination receives the event, it wraps the function's input and output in an envelope. This is the full structure that arrives at the SQS queue or EventBridge event:

// OnSuccess payload envelope (abbreviated)
{
  "version": "1.0",
  "timestamp": "2026-10-24T12:30:00.000Z",
  "requestContext": {
    "requestId": "a1b2c3d4-...",
    "functionArn": "arn:aws:lambda:us-east-1:123456789012:function:ProbeWorker:live",
    "condition": "Success",           // "Success" or "RetriesExhausted"
    "approximateInvokeCount": 1       // how many times Lambda tried before this outcome
  },
  "requestPayload": {                 // the original event that triggered the function
    "endpoint": "https://example.com/mcp",
    "serverId": "example-mcp-server"
  },
  "responseContext": {
    "statusCode": 200,
    "executedVersion": "5"
  },
  "responsePayload": {                // the function's return value (OnSuccess) or error (OnFailure)
    "status": "healthy",
    "latencyMs": 142,
    "toolCount": 7,
    "schemaHash": "a3f9..."
  }
}

// OnFailure payload — same envelope, but responsePayload contains the error
{
  "requestContext": {
    "condition": "RetriesExhausted",
    "approximateInvokeCount": 3       // function was retried 2 times before giving up
  },
  "responsePayload": {
    "errorType": "ProbeTimeoutError",
    "errorMessage": "MCP endpoint did not respond within 30 seconds",
    "stackTrace": ["..."]
  }
}

The requestPayload is always the original event — for an EventBridge-triggered probe, this is the EventBridge event detail. The responsePayload on failure is the error object your function threw (or the Lambda service error if the function was throttled or out of memory). A bare DLQ only receives requestPayload — no function response or error details.

CDK configuration

import * as lambda from "aws-cdk-lib/aws-lambda";
import * as lambdaDestinations 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";
import * as targets from "aws-cdk-lib/aws-events-targets";
import { Duration } from "aws-cdk-lib";

// Probe result queue (success)
const probeResultsQueue = new sqs.Queue(this, "ProbeResults", {
  visibilityTimeout: Duration.seconds(30),
  retentionPeriod: Duration.days(7),
});

// Dead-letter queue (failures)
const probeDlq = new sqs.Queue(this, "ProbeDlq", {
  retentionPeriod: Duration.days(14),
});

const probeWorkerFn = new lambda.Function(this, "ProbeWorker", {
  // ... handler, runtime, etc.

  // Retry config: for probe workers, 0 retries avoids duplicate health checks
  // and is safer for idempotency (two concurrent pings may cause false alerts)
  maxEventAge: Duration.minutes(5),  // drop events older than 5 min — stale probes are useless
  retryAttempts: 0,
});

// Configure Destinations
probeWorkerFn.configureAsyncInvoke({
  onSuccess: new lambdaDestinations.SqsDestination(probeResultsQueue),
  onFailure: new lambdaDestinations.SqsDestination(probeDlq),
  retryAttempts: 0,                  // also settable here (overrides function-level)
  maxEventAge: Duration.minutes(5),
});

// EventBridge rule triggers the probe every 60 seconds
const rule = new events.Rule(this, "ProbeSchedule", {
  schedule: events.Schedule.rate(Duration.minutes(1)),
});
rule.addTarget(new targets.LambdaFunction(probeWorkerFn, {
  event: events.RuleTargetInput.fromObject({
    endpoint: "https://example.com/mcp",
    serverId: "example-mcp-server",
  }),
}));

Grant the probe function permission to send to both queues, and grant the Lambda service principal permission to write to the destinations. CDK's SqsDestination handles the IAM grants automatically.

DLQ vs Destinations: when to use each

FeatureDLQ (Dead Letter Queue)OnFailure Destination
Payload capturedOriginal event onlyOriginal event + error type + stack trace + invocation count
Success routingNo (failures only)Yes (OnSuccess destination available)
Retry count before deliveryAfter all Lambda retriesAfter all Lambda retries (same timing)
Supported targetsSQS, SNS onlySQS, SNS, EventBridge, Lambda
Can you use both?Yes — DLQ and Destinations are independentYes — configuring both routes failures to both

In practice, use Destinations for new architectures and DLQ only for backwards compatibility with existing consumers that expect the bare event payload. Configuring both is valid but usually redundant — pick one failure handling approach and stick with it.

Common failure modes

SymptomCauseFix
Destination not invoked despite function completingFunction was invoked synchronously (RequestResponse), not asynchronouslyVerify the trigger is async; check aws lambda list-event-source-mappings and invoke type
OnFailure destination not receiving failuresretryAttempts is still 2 (default); failures are retried twice before going to OnFailureSet retryAttempts: 0 in configureAsyncInvoke if immediate failure routing is needed
SQS destination receives events but message body is not parseableSQS message body is the full Destinations envelope (JSON string), not the original eventParse the envelope: JSON.parse(message.Body).responsePayload for the function's return value
EventBridge destination events not matching any ruleDestination events use the source lambda and detail-type Lambda Function Invocation Result - Success — rules must match this exactlyUse aws events put-rule --event-pattern '{"source":["lambda"]}' to capture all Lambda destinations
Destination receives event from wrong function versionAlias points to old version; probe result includes old executedVersionRedeploy alias to new version; check requestContext.functionArn contains correct alias qualifier