A Lambda function sitting idle does nothing. What makes it useful is what triggers it.
Triggers are the connective tissue of serverless architectures. They define how your function integrates with the rest of your system — whether that's responding to an HTTP request, reacting to a file upload, processing a message queue, or running on a schedule.
In this article, we'll break down the most important Lambda trigger types, explain the critical difference between synchronous and asynchronous invocation, and show you real-world patterns for each.
Synchronous vs Asynchronous Invocation
Before diving into specific triggers, you need to understand the most fundamental distinction in Lambda invocation models.
Synchronous Invocation
The caller waits. Lambda executes your function immediately and returns the result directly to the caller.
Client → API Gateway → Lambda → (executes) → returns response → API Gateway → Client
[waits..................................]
Behavior on error: Lambda returns the error immediately to the caller. No automatic retry. The caller is responsible for retry logic.
AWS triggers that use synchronous invocation:
- API Gateway (REST API & HTTP API)
- Application Load Balancer
- Lambda Function URLs
- Cognito (custom auth triggers)
- CloudFront (Lambda@Edge)
Asynchronous Invocation
The caller doesn't wait. Lambda places the event in an internal queue and immediately returns a 202 Accepted. A separate process picks up the event and executes your function.
Client → S3 Event → Lambda Queue → (executes eventually) → [no response to caller]
[returns 202 immediately]
Behavior on error: Lambda automatically retries up to 2 times with exponential backoff. If all retries fail, the event can be routed to a Dead Letter Queue (DLQ) or an EventBridge Pipes destination for custom failure handling.
AWS triggers that use asynchronous invocation:
- S3 (object events)
- SNS
- EventBridge (scheduled rules & event bus)
- SES
- CloudWatch Logs (subscription filters)
Why this matters: Choosing the wrong invocation model leads to silent data loss (no DLQ on async failures) or poor user experience (blocking calls that should be async). Know your model before you build.
Trigger Type 1: Scheduled Triggers (EventBridge Rules)
The simplest trigger type. Run your function on a time-based schedule — like cron, but serverless.
Common use cases:
- Hourly batch data collection and report generation
- Daily cleanup jobs (purge expired records at midnight)
- Periodic health checks and monitoring
- Sending scheduled notifications
# handler.py
import boto3
import json
from datetime import datetime, timedelta
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('brand_metrics')
def handler(event, context):
"""
Triggered by EventBridge every hour.
Aggregates brand API usage metrics and writes a summary record.
"""
now = datetime.utcnow()
one_hour_ago = now - timedelta(hours=1)
# Query last hour's invocations
response = table.query(
IndexName='timestamp-index',
KeyConditionExpression='#ts BETWEEN :start AND :end',
ExpressionAttributeNames={'#ts': 'timestamp'},
ExpressionAttributeValues={
':start': one_hour_ago.isoformat(),
':end': now.isoformat()
}
)
total_calls = len(response['Items'])
# Write hourly summary
table.put_item(Item={
'id': f'summary-{now.strftime("%Y%m%d-%H")}',
'timestamp': now.isoformat(),
'total_calls': total_calls,
'type': 'hourly_summary'
})
print(f'Hourly summary written: {total_calls} calls in the last hour')
return {'processed': total_calls}
# serverless.yml
functions:
hourlyMetrics:
handler: handler.handler
events:
- schedule:
rate: rate(1 hour) # every hour
# Or use cron syntax:
# - schedule:
# rate: cron(0 0 * * ? *) # midnight UTC daily
Key behaviors:
- EventBridge scheduled rules use asynchronous invocation
- If your function fails, Lambda retries up to 2 times
- Always configure a DLQ for scheduled jobs — silent failures in batch jobs are dangerous
# Add a DLQ for failure handling
functions:
hourlyMetrics:
handler: handler.handler
destinations:
onFailure: arn:aws:sqs:us-east-1:123456789:hourly-metrics-dlq
events:
- schedule:
rate: rate(1 hour)
Trigger Type 2: S3 Object Triggers
S3 triggers fire when objects are created, modified, or deleted in a bucket. This is one of the most powerful async trigger patterns in AWS.
Common use cases:
- Image/video transcoding when a file is uploaded
- Automatic data pipeline ingestion (CSV → DynamoDB)
- Log file processing
- Thumbnail generation
# handler.py
import boto3
import json
import urllib.parse
s3 = boto3.client('s3')
rekognition = boto3.client('rekognition')
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('brand_logos')
def handler(event, context):
"""
Triggered when a new logo image is uploaded to S3.
Runs Rekognition label detection and stores results in DynamoDB.
"""
for record in event['Records']:
bucket = record['s3']['bucket']['name']
key = urllib.parse.unquote_plus(record['s3']['object']['key'])
print(f'Processing: s3://{bucket}/{key}')
# Run Rekognition label detection
response = rekognition.detect_labels(
Image={'S3Object': {'Bucket': bucket, 'Name': key}},
MaxLabels=10,
MinConfidence=80
)
labels = [label['Name'] for label in response['Labels']]
# Store results in DynamoDB
brand_id = key.split('/')[0] # e.g., "nike/logo.png" → "nike"
table.put_item(Item={
'brandId': brand_id,
'imageKey': key,
'labels': labels,
'processedAt': context.aws_request_id
})
print(f'Stored {len(labels)} labels for brand: {brand_id}')
return {'processed': len(event['Records'])}
# serverless.yml
functions:
processLogo:
handler: handler.handler
events:
- s3:
bucket: brand-assets-bucket
event: s3:ObjectCreated:* # fires on any upload
rules:
- prefix: logos/ # only files in logos/ folder
- suffix: .png # only PNG files
S3 event filter rules — this is where S3 triggers get powerful:
| Filter | Example | Effect |
|---|---|---|
prefix |
logos/ |
Only trigger for objects in the logos/ folder |
suffix |
.mp4 |
Only trigger for MP4 files |
| Combined |
prefix: raw/ + suffix: .csv
|
Only raw CSV uploads |
Key behaviors:
- S3 triggers use asynchronous invocation
- S3 guarantees at-least-once delivery — your function may receive duplicate events for the same object
- Always make your S3 handlers idempotent (safe to run twice on the same object)
# Idempotent pattern: check if already processed
def handler(event, context):
for record in event['Records']:
key = record['s3']['object']['key']
etag = record['s3']['object']['eTag']
# Check if this exact version was already processed
existing = table.get_item(Key={'imageKey': key})
if existing.get('Item', {}).get('etag') == etag:
print(f'Already processed {key} (etag: {etag}), skipping')
continue
# Process...
Trigger Type 3: API Gateway (Synchronous HTTP Triggers)
API Gateway + Lambda is the most common serverless pattern — over 70% of serverless applications use this combination. It's how you build HTTP APIs without managing servers.
Two options in AWS:
| REST API (v1) | HTTP API (v2) | |
|---|---|---|
| Latency | ~6ms added | ~1ms added |
| Cost | Higher | ~70% cheaper |
| Features | Full (WAF, caching, usage plans) | Simplified |
| Payload format | v1 | v2 |
| Best for | Enterprise APIs needing advanced features | Most modern APIs |
# handler.py — HTTP API (v2 payload format)
import json
import boto3
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table('brands')
def handler(event, context):
"""
GET /brand/{brandId}
Returns brand metadata from DynamoDB.
"""
# HTTP API v2 event structure
brand_id = event['pathParameters']['brandId']
http_method = event['requestContext']['http']['method']
if http_method != 'GET':
return {
'statusCode': 405,
'body': json.dumps({'error': 'Method not allowed'})
}
response = table.get_item(Key={'brandId': brand_id})
if 'Item' not in response:
return {
'statusCode': 404,
'headers': {'Content-Type': 'application/json'},
'body': json.dumps({'error': f'Brand {brand_id} not found'})
}
return {
'statusCode': 200,
'headers': {
'Content-Type': 'application/json',
'Cache-Control': 'max-age=300' # 5-minute client cache
},
'body': json.dumps(response['Item'])
}
# serverless.yml
functions:
getBrand:
handler: handler.handler
events:
- httpApi: # HTTP API v2 (recommended)
path: /brand/{brandId}
method: GET
# For REST API v1:
# - http:
# path: /brand/{brandId}
# method: GET
# cors: true
Key behaviors:
- API Gateway uses synchronous invocation
- Lambda must respond within 29 seconds (API Gateway hard timeout)
- On Lambda error, API Gateway returns a
502 Bad Gatewayto the client - The client is responsible for retry logic
Lambda Function URLs — a simpler alternative for single-function APIs:
# No API Gateway needed — Lambda gets a direct HTTPS endpoint
functions:
getBrand:
handler: handler.handler
url:
cors: true
authorizer: aws_iam # or 'none' for public
Trigger Type 4: SQS (Message Queue Triggers)
SQS triggers are the backbone of reliable async processing. Lambda polls your SQS queue and invokes your function in batches.
Common use cases:
- Decoupling microservices
- Processing webhook payloads
- Fan-out patterns (SNS → multiple SQS queues → multiple Lambda functions)
- Rate-limiting downstream API calls
# handler.py
import json
import boto3
from typing import Any
ses = boto3.client('ses')
def handler(event, context):
"""
Processes brand notification messages from SQS.
Sends email notifications for brand update events.
"""
# SQS delivers messages in batches
successful = []
failed = []
for record in event['Records']:
message_id = record['messageId']
try:
body = json.loads(record['body'])
brand_id = body['brandId']
event_type = body['eventType'] # e.g., 'logo_updated'
recipient = body['notifyEmail']
# Send notification email
ses.send_email(
Source='noreply@yourdomain.com',
Destination={'ToAddresses': [recipient]},
Message={
'Subject': {'Data': f'Brand Update: {brand_id}'},
'Body': {'Text': {'Data': f'Event: {event_type} for brand {brand_id}'}}
}
)
successful.append(message_id)
print(f'Notification sent for {brand_id} ({event_type})')
except Exception as e:
print(f'Failed to process message {message_id}: {e}')
failed.append({'itemIdentifier': message_id})
# Partial batch failure response — only failed messages go back to queue
# Requires "functionResponseTypes: [ReportBatchItemFailures]" in config
return {'batchItemFailures': failed}
# serverless.yml
functions:
processBrandNotifications:
handler: handler.handler
events:
- sqs:
arn: arn:aws:sqs:us-east-1:123456789:brand-notifications
batchSize: 10 # process up to 10 messages at once
maximumBatchingWindow: 5 # wait up to 5s to fill the batch
functionResponseType: ReportBatchItemFailures # partial batch failure support
SQS + Lambda retry behavior:
Message arrives in SQS
│
▼
Lambda polls & invokes function
│
SUCCESS → message deleted from queue ✅
│
FAILURE → message becomes visible again after visibility timeout
│
▼
Retry (up to maxReceiveCount, e.g., 3 times)
│
Still failing → message moved to Dead Letter Queue (DLQ) ⚠️
Always configure a DLQ for SQS:
# serverless.yml — SQS queue with DLQ
resources:
Resources:
BrandNotificationsQueue:
Type: AWS::SQS::Queue
Properties:
QueueName: brand-notifications
VisibilityTimeout: 60
RedrivePolicy:
deadLetterTargetArn: !GetAtt BrandNotificationsDLQ.Arn
maxReceiveCount: 3 # move to DLQ after 3 failures
BrandNotificationsDLQ:
Type: AWS::SQS::Queue
Properties:
QueueName: brand-notifications-dlq
MessageRetentionPeriod: 1209600 # keep failed messages for 14 days
Trigger Type 5: EventBridge Event Bus (Event-Driven Architecture)
EventBridge is the most powerful trigger type for building loosely coupled, event-driven systems. It acts as a central event router — services publish events, and EventBridge routes them to the right Lambda functions based on rules.
Common use cases:
- Microservice decoupling (services communicate via events, not direct calls)
- State change tracking across services
- SaaS integrations (Stripe, GitHub webhooks → EventBridge → Lambda)
- Fan-out to multiple consumers
# publisher.py — any service can publish events
import boto3
import json
from datetime import datetime
events = boto3.client('events')
def publish_brand_updated(brand_id: str, changes: dict):
"""Publish a brand update event to EventBridge"""
events.put_events(
Entries=[{
'Source': 'com.yourdomain.brand-service',
'DetailType': 'BrandUpdated',
'Detail': json.dumps({
'brandId': brand_id,
'changes': changes,
'updatedAt': datetime.utcnow().isoformat()
}),
'EventBusName': 'brand-platform-bus'
}]
)
# consumer.py — Lambda function triggered by EventBridge rule
import json
def handler(event, context):
"""
Triggered when a BrandUpdated event is published.
Invalidates CDN cache for the updated brand's assets.
"""
detail = event['detail']
brand_id = detail['brandId']
changes = detail['changes']
print(f'Brand {brand_id} updated: {changes}')
if 'logoUrl' in changes:
# Logo changed — invalidate CDN cache
invalidate_cdn_cache(brand_id)
if 'colors' in changes:
# Color palette changed — regenerate color variants
trigger_color_regeneration(brand_id)
return {'handled': True}
# serverless.yml
functions:
handleBrandUpdated:
handler: consumer.handler
events:
- eventBridge:
eventBus: brand-platform-bus
pattern:
source:
- com.yourdomain.brand-service
detail-type:
- BrandUpdated
# Optional: filter to specific brands
detail:
brandId:
- prefix: enterprise-
EventBridge vs SQS — when to use which:
| Scenario | Use |
|---|---|
| One producer, one consumer | SQS |
| One event, multiple consumers | EventBridge (fan-out) |
| Need message ordering | SQS FIFO |
| Need content-based routing | EventBridge (filter rules) |
| Cross-account event delivery | EventBridge |
| Rate-limiting / throttling | SQS |
Choosing the Right Trigger: A Decision Guide
What initiates your Lambda function?
│
├── HTTP request from client/browser
│ └── → API Gateway HTTP API or Function URL
│
├── File uploaded / object changed in S3
│ └── → S3 Event Notification (async)
│
├── Run on a schedule (cron)
│ └── → EventBridge Scheduled Rule (async)
│
├── Message in a queue (reliable processing)
│ └── → SQS Trigger (polling, batched)
│
├── Event from another service (decoupled)
│ └── → EventBridge Event Bus (async, fan-out)
│
└── Stream of records (Kinesis / DynamoDB Streams)
└── → Kinesis / DynamoDB Streams Trigger (polling)
Summary
| Trigger | Invocation | Retry | Best For |
|---|---|---|---|
| API Gateway | Synchronous | ❌ Caller handles | HTTP APIs, web apps |
| S3 | Asynchronous | ✅ 2 auto retries | File processing pipelines |
| EventBridge Schedule | Asynchronous | ✅ 2 auto retries | Cron jobs, batch tasks |
| SQS | Polling (async) | ✅ Queue visibility + DLQ | Reliable message processing |
| EventBridge Event Bus | Asynchronous | ✅ 2 auto retries | Event-driven microservices |
The trigger you choose determines your invocation model, retry behavior, and error handling strategy. Getting this right is the difference between a resilient serverless system and one that silently drops data under failure.
Always configure a DLQ or failure destination for async triggers. Silent failures are the #1 operational issue in serverless production systems.
Next in this series: **Part 3 — Auto Scaling in AWS Lambda: Concurrency, Throttling & Scale-to-Zero**
Top comments (0)