Lambda Triggers & Event Sources
A Lambda function (a small piece of code AWS runs for you) does nothing on its own — something has to invoke it. That “something” is a trigger, and the service that fires the trigger is the event source. This page surveys the most common event sources and, more importantly, explains the three different ways Lambda gets invoked. The way you connect matters because retry and error behavior differ enormously between them — getting this wrong is the single most common reason engineers lose events without noticing.
The three invocation models
Before picking a trigger, you must know how it invokes your function. There are exactly three models, and each handles failure differently.
| Model | How it works | Who uses it | Retry behavior on error |
|---|---|---|---|
| Synchronous (push) | Caller invokes, waits for the response | API Gateway, Application Load Balancer, direct SDK calls | None by Lambda — the caller sees the error and decides what to do |
| Asynchronous (push) | Caller hands the event to Lambda and walks away | S3 events, SNS, EventBridge | Lambda retries twice (3 attempts total), then sends to a dead-letter queue (DLQ) if configured |
| Poll-based (event source mapping) | Lambda polls the source and pulls batches | SQS, DynamoDB Streams, Kinesis, Amazon MSK (Managed Streaming for Kafka) | Keeps retrying until the message expires or the batch succeeds |
A DLQ is a “dead-letter queue” — a holding place for events that failed every attempt, so you can inspect them later instead of losing them.
The big gotcha: Do not assume “Lambda retries for me.” A synchronous caller gets the error thrown straight back and Lambda retries nothing. An asynchronous source retries only twice. A poll-based SQS source can retry the same message for days until it expires. Always confirm which model your trigger uses before you rely on retries.
Push-based sources (Lambda does not poll)
In push-based sources, the event source calls Lambda’s Invoke API directly. You attach a resource-based policy (a permission statement on the function that says “this service is allowed to invoke me”).
API Gateway — synchronous
API Gateway turns an HTTP request into a Lambda invocation and returns your function’s response to the caller. This is synchronous: if your function errors or times out, the user gets a 502 or 504. Use it for REST and HTTP APIs and webhooks. See Build a serverless API for the full setup.
S3 events — asynchronous
S3 (Simple Storage Service, AWS object storage) can invoke a function whenever an object is created or deleted. Use it for processing uploads (resize an image, scan a file). Because it is asynchronous, a failed invoke retries twice then disappears unless you configure a DLQ.
Console steps:
- Open the Lambda console and select your function.
- Click Add trigger.
- Choose S3 as the source.
- Pick your Bucket, set Event type to
PUT(all object-create events). - (Optional) add a Prefix like
uploads/so only those keys trigger it. - Acknowledge the recursive-invocation warning and click Add.
The CLI equivalent has two parts. First give S3 permission to invoke the function, then wire up the bucket notification:
aws lambda add-permission \
--function-name process-upload \
--principal s3.amazonaws.com \
--statement-id s3invoke \
--action lambda:InvokeFunction \
--source-arn arn:aws:s3:::my-upload-bucket-0a1b2c3d \
--source-account 111122223333
Output:
{
"Statement": "{\"Sid\":\"s3invoke\",\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"s3.amazonaws.com\"},\"Action\":\"lambda:InvokeFunction\",\"Resource\":\"arn:aws:lambda:us-east-1:111122223333:function:process-upload\"}"
}
aws s3api put-bucket-notification-configuration \
--bucket my-upload-bucket-0a1b2c3d \
--notification-configuration '{
"LambdaFunctionConfigurations": [{
"LambdaFunctionArn": "arn:aws:lambda:us-east-1:111122223333:function:process-upload",
"Events": ["s3:ObjectCreated:Put"]
}]
}'
SNS and EventBridge — asynchronous
SNS (Simple Notification Service, a pub/sub messaging service) and EventBridge (a serverless event bus) both push events to Lambda asynchronously. Use SNS for fan-out (one message to many subscribers). Use EventBridge for routing AWS service events or scheduled cron-style jobs. See Amazon SNS and EventBridge.
Poll-based sources (event source mappings)
Here Lambda creates a managed poller that reads from the source in batches and invokes your function. You configure an event source mapping (ESM) — a connection object owned by Lambda. The source never calls Lambda; Lambda pulls from it.
SQS
SQS (Simple Queue Service) is a message queue. Lambda polls it, pulls a batch (up to 10,000 records, default 10), and invokes your function once per batch. If the function throws, the messages become visible again and are retried — until the queue’s retention period expires (up to 14 days). Configure a redrive policy with a maxReceiveCount so poison messages move to a DLQ instead of looping forever.
aws lambda create-event-source-mapping \
--function-name process-orders \
--event-source-arn arn:aws:sqs:us-east-1:111122223333:orders-queue \
--batch-size 10 \
--function-response-types ReportBatchItemFailures
Output:
{
"UUID": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
"BatchSize": 10,
"EventSourceArn": "arn:aws:sqs:us-east-1:111122223333:orders-queue",
"FunctionArn": "arn:aws:lambda:us-east-1:111122223333:function:process-orders",
"State": "Creating",
"FunctionResponseTypes": ["ReportBatchItemFailures"]
}
ReportBatchItemFailures lets your function return only the IDs that failed, so successful messages in the batch are not retried. Without it, one bad message reprocesses the whole batch.
DynamoDB Streams and Kinesis
DynamoDB Streams emit a record for every change to a table; Kinesis Data Streams carry high-volume real-time data. Both are ordered per shard, so Lambda processes one batch at a time per shard and, by default, retries a failing batch until it succeeds or the data expires — blocking that shard. Always set MaximumRetryAttempts and an OnFailure destination so a poison record cannot stall the stream. See DynamoDB Streams.
Cost note: Poll-based mappings are billed for the invocations and duration, not the polling itself — SQS/stream polling is free. But a tight retry loop on a poison message can rack up millions of invocations. At roughly $0.20 per million requests plus duration, a stuck DynamoDB shard retrying for hours is a real (and avoidable) bill. Always cap retries.
Best practices
- Identify the invocation model (sync / async / poll) for every trigger before you write error handling — it dictates everything about retries.
- Configure a DLQ or
OnFailuredestination on every asynchronous and stream-based trigger so failed events are captured, not silently dropped. - For SQS, set
maxReceiveCounton a redrive policy and useReportBatchItemFailuresto avoid reprocessing whole batches. - For DynamoDB/Kinesis, cap
MaximumRetryAttemptsand setBisectBatchOnFunctionErrorso one bad record cannot poison an entire batch. - Keep handlers idempotent (safe to run twice on the same event) — every model can deliver an event more than once.
- Grant the narrowest permission: scope
add-permissionto a specific--source-arn, never a wildcard. - Watch the
IteratorAge(streams) and queue depth (SQS) CloudWatch metrics to catch a stuck or falling-behind consumer early.