Debug Lambda timeouts and crashes
See how each Lambda invocation ended for each SQS record, including timeouts and out-of-memory kills the handler never saw.
Use this when an SQS-triggered Lambda function fails, retries, times out, or a message "disappears" between the queue and your handler.
1Step 1: Wrap the handler#
The wrapper records each SQS record twice: a trailbeam receive before your handler runs, and a trailbeam outcome after it returns or throws.
import trailbeam.aws_lambda
@trailbeam.aws_lambda.wrap
def handler(event, context):
failed = []
for record in event["Records"]:
try:
process(record)
except RetryableError:
failed.append({"itemIdentifier": record["messageId"]})
return {"batchItemFailures": failed}const trailbeam = require('@trailbeam/sdk');
exports.handler = trailbeam.wrapHandler(async (event, context) => {
const batchItemFailures = [];
for (const record of event.Records) {
try { await process(record); } catch (e) { batchItemFailures.push({ itemIdentifier: record.messageId }); }
}
return { batchItemFailures };
});| Option (Python / Node.js) | Type | Default | What it does |
|---|---|---|---|
receipt_hash_key / receiptHashKey |
string | TRAILBEAM_RECEIPT_HASH_KEY, else the published default key |
Key for the receipt-handle hash |
flush_timeout_millis / flushTimeoutMs |
int | 2000 |
Longest wait for each flush |
report_batch_item_failures / reportBatchItemFailures |
bool | true |
Read batchItemFailures from the result. Set to match the event source mapping's ReportBatchItemFailures. |
span_exporter / spanExporter |
exporter | OTLP/HTTP to TRAILBEAM_OTLP_TRACES_ENDPOINT |
Replace the exporter |
span_processor / spanProcessor |
processor | batch processor | Replace the processor |
Set TRAILBEAM_OTLP_TRACES_ENDPOINT on the function (default http://localhost:4318/v1/traces) to a Collector or Trailbeam address the function can reach.
2Step 2: Know what the wrapper records#
For each record whose eventSource is aws:sqs, before the handler runs:
- one
trailbeam receivewithmessaging.operation.nameLambdaEvent,trailbeam.sourcelambda,trailbeam.receive_count,trailbeam.sent_timestamp_ms,trailbeam.first_receive_timestamp_ms, the receipt-handle hash,trailbeam.queue.arn(the record'seventSourceARN),faas.name(from the context, elseAWS_LAMBDA_FUNCTION_NAME) andfaas.invocation_id(aws_request_id/awsRequestId); - the span is zero-length, at the moment the event arrived, with
trailbeam.start_observed = false. Lambda's poller made theReceiveMessagecall, so only an upper bound is known. The screen shows "no later than" that time; - the wrapper flushes these spans before calling your handler, so a kill cannot lose them.
After the handler, one trailbeam outcome per record, flushed again:
trailbeam.outcome |
When |
|---|---|
| success | The handler returned, and the record is not in a valid batchItemFailures |
| failed_reported | The record's messageId is in batchItemFailures |
| failed_invalid_response | batchItemFailures is not a list, or one entry has an itemIdentifier that is missing, empty, not a string or not in the batch. Every record gets this value, because Lambda fails the whole batch. |
| failed_exception | The handler threw. Every record gets this value. trailbeam.error_type is the class name only, never the message. |
| no outcome | The process died first: timeout, out of memory, or in Python a BaseException such as SystemExit |
messaging.client.id is AWS_LAMBDA_LOG_STREAM_NAME, so each execution environment is one consumer. If that variable is not set, it is the wrapper's service.instance.id. Events with no SQS records pass through untouched.
3Step 3: Tell Trailbeam your function's timeout#
Trailbeam infers a kill when a Lambda receive has no trailbeam outcome for the same faas.invocation_id once the Lambda timeout has passed. Trailbeam's timeout defaults to 900 s (the Lambda maximum), because spans do not carry your function's real timeout.
trailbeam-store --lambda-timeout 30 # or TRAILBEAM_LAMBDA_TIMEOUT_S=30Or per request, from 1 to 86400:
curl -s "http://127.0.0.1:4318/api/messages/$MESSAGE_ID?lambda_timeout_s=30"The web page takes the same parameter: /message?id=...&lambda_timeout_s=30.
4Step 4: Read the timeline#
A record whose first invocation timed out (function timeout 30 s, queue visibility timeout 60 s):
- 08:15:00.004Sentorders-api · queue orders
- 08:15:00.380Receivedno later than · delivery 1 · receive count 1 · function orders-fn
- inferredLambda invocation ended without an outcomeA Lambda receive with no outcome after 30 s: the invocation most likely timed out or ran out of memory.
- inferredVisibility timeout expiredafter delivery 1, before delivery 2
- 08:16:01.120Receivedno later than · delivery 2 · receive count 2 · invocation 4b1e
- 08:16:01.940Lambda outcomedelivery 2 · outcome success
- inferredDeleted by LambdaThe handler succeeded for this record, so Lambda's event source mapping deletes the message. That delete is not observed.
The kill's window runs from the receive's end to the receive's end plus the Lambda timeout. It carries delivery and faas_invocation_id, so you can find the invocation in CloudWatch Logs. The final state is deleted_inferred.
| What the timeline shows | Cause | Fix |
|---|---|---|
| Received, then inferred Lambda invocation ended without an outcome | Timeout or out-of-memory kill. Nothing inside the process can see it. | Check the invocation's REPORT line. Raise the function's timeout or memory. |
Received, nothing else, state processing |
The invocation is still within the Lambda timeout | Wait, or set --lambda-timeout to the real value |
Lambda outcome failed_exception with error_type |
The handler threw. Every record in the batch is retried. | Catch per record and return batchItemFailures |
| Lambda outcome failed_invalid_response on every record | A batchItemFailures entry is malformed or names an ID not in the batch |
Return {"itemIdentifier": record["messageId"]} for failed records only |
| Lambda outcome failed_reported, expiry, a new delivery | You reported the record as failed. SQS redelivers it after the visibility timeout. | Expected for a retry. Watch the receive count against maxReceiveCount. |
Lambda outcome failed_reported or failed_invalid_response, but no redelivery ever comes and the message stays not_deleted |
The event source mapping does not use ReportBatchItemFailures. Lambda ignored batchItemFailures and deleted the batch, unobserved. |
Set report_batch_item_failures=False / reportBatchItemFailures: false, or enable it on the mapping |
Lambda outcome with no delivery |
Its receive span never reached Trailbeam (for example a flush timed out) | Check the function can reach TRAILBEAM_OTLP_TRACES_ENDPOINT |
5Step 5: Find invocations that are still open or killed#
processing means the latest delivery is a Lambda receive, inside the timeout, with no outcome. Such messages are also on the stuck list once they pass --stuck-after (300 s), because there is no success yet.
curl -s 'http://127.0.0.1:4318/api/stuck?lambda_timeout_s=30' \
| jq '.messages[] | {message_id, state, deliveries, age_s}'Prevent it#
Return partial batch failures instead of throwing. A throw marks every record failed_exception and retries the whole batch. A batchItemFailures entry retries only that record.
Match the wrapper to the event source mapping. If FunctionResponseTypes does not include ReportBatchItemFailures, Lambda ignores batchItemFailures, and so must Trailbeam:
@trailbeam.aws_lambda.wrap(report_batch_item_failures=False)
def handler(event, context): ...exports.handler = trailbeam.wrapHandler(handler, { reportBatchItemFailures: false });Budget for the flushes. Each invocation flushes twice (receives, then outcomes). Each flush waits at most the flush timeout (default 2000 ms), so an unreachable endpoint can add up to about 4 s to an invocation. Lower it if your function's timeout is tight:
exports.handler = trailbeam.wrapHandler(handler, { flushTimeoutMs: 500 });@trailbeam.aws_lambda.wrap(flush_timeout_millis=500)
def handler(event, context): ...Related#
- AWS Lambda setup
- Recorded and inferred steps
- Find out why a message ran twice
- Span contract: outcome
Message
Amazon SQS terms in Vocabulary
Queue
Amazon SQS terms in Vocabulary
Handler
Amazon SQS terms in Vocabulary
Record
AWS Lambda terms in Vocabulary
Receipt-handle hash
Batch item failures
ReportBatchItemFailures turned on, your handler can return the IDs of the records that failed. Lambda deletes the rest and leaves those to become visible again. Without it, any error fails the whole batch.AWS Lambda terms in Vocabulary
Event source mapping
AWS Lambda terms in Vocabulary
OTLP
OpenTelemetry terms in Vocabulary
Collector
OpenTelemetry terms in Vocabulary
Span
OpenTelemetry terms in Vocabulary
Consumer
ReceiveMessage) and then deletes them. A Lambda function behind an event source mapping is a consumer too.Amazon SQS terms in Vocabulary
Receive
ReceiveMessage call returning a message. A receive does not remove the message; it starts its visibility timeout.Amazon SQS terms in Vocabulary
Function timeout
Task timed out after N seconds.AWS Lambda terms in Vocabulary
Visibility timeout
Amazon SQS terms in Vocabulary
Inferred
inferred: true. See Recorded and inferred steps.Delivery
Receive count
ApproximateReceiveCount: how many times SQS has returned the message. It goes up by one on every receive.Amazon SQS terms in Vocabulary
maxReceiveCount
Amazon SQS terms in Vocabulary
SentTimestamp
Amazon SQS terms in Vocabulary
ApproximateFirstReceiveTimestamp