DEV Community

James Lee
James Lee

Posted on

Lambda Triggers Explained: S3, EventBridge, API Gateway & SQS

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..................................]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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'])}
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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...
Enter fullscreen mode Exit fullscreen mode

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'])
    }
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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 Gateway to 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
Enter fullscreen mode Exit fullscreen mode

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}
Enter fullscreen mode Exit fullscreen mode
# 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
Enter fullscreen mode Exit fullscreen mode

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) ⚠️
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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'
        }]
    )
Enter fullscreen mode Exit fullscreen mode
# 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}
Enter fullscreen mode Exit fullscreen mode
# 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-
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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)