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:
- The caller uses
InvocationType: Eventin the SDK (InvokeCommandwithInvocationType) - The trigger is SNS, S3 event notification, EventBridge event bus rule, or IoT rule
- The function is invoked via SQS event source mapping (the mapping itself invokes the function synchronously, but from the perspective of the original SQS producer, the processing is async)
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
| Target | OnSuccess | OnFailure | Best for |
|---|---|---|---|
| SQS queue | Yes | Yes | Batch processing of results; DLQ-style failure collection with full context |
| SNS topic | Yes | Yes | Fan-out to multiple subscribers; email/Slack notification on failure |
| EventBridge event bus | Yes | Yes | Complex routing rules; multiple downstream consumers; audit trail |
| Lambda function | Yes | Yes | Chaining: 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
| Feature | DLQ (Dead Letter Queue) | OnFailure Destination |
|---|---|---|
| Payload captured | Original event only | Original event + error type + stack trace + invocation count |
| Success routing | No (failures only) | Yes (OnSuccess destination available) |
| Retry count before delivery | After all Lambda retries | After all Lambda retries (same timing) |
| Supported targets | SQS, SNS only | SQS, SNS, EventBridge, Lambda |
| Can you use both? | Yes — DLQ and Destinations are independent | Yes — 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
| Symptom | Cause | Fix |
|---|---|---|
| Destination not invoked despite function completing | Function was invoked synchronously (RequestResponse), not asynchronously | Verify the trigger is async; check aws lambda list-event-source-mappings and invoke type |
| OnFailure destination not receiving failures | retryAttempts is still 2 (default); failures are retried twice before going to OnFailure | Set retryAttempts: 0 in configureAsyncInvoke if immediate failure routing is needed |
| SQS destination receives events but message body is not parseable | SQS message body is the full Destinations envelope (JSON string), not the original event | Parse the envelope: JSON.parse(message.Body).responsePayload for the function's return value |
| EventBridge destination events not matching any rule | Destination events use the source lambda and detail-type Lambda Function Invocation Result - Success — rules must match this exactly | Use aws events put-rule --event-pattern '{"source":["lambda"]}' to capture all Lambda destinations |
| Destination receives event from wrong function version | Alias points to old version; probe result includes old executedVersion | Redeploy alias to new version; check requestContext.functionArn contains correct alias qualifier |