Right, let’s talk about getting your Lambda function to actually do something. It’s not just going to sit there in its virtual serverless condo, waiting for a polite invitation. It needs a trigger. An event source is that doorbell, that alarm clock, that… well, you get the idea. It’s the thing that tells your function, “Hey, wake up, we’ve got work to do.” We’re going to walk through the big ones, and I’ll tell you not just how they work, but the bizarre little quirks you’ll only learn by getting burned by them at 2 AM.

The S3 Object Ballet

When you configure an S3 bucket as an event source, you’re essentially asking S3 to perform a delicate ballet. Every time a specific event occurs (like s3:ObjectCreated:*), S3 has to gracefully toss a JSON payload over the fence to your Lambda function’s event queue. The magic here is that S3 itself is invoking your function; it’s not you setting up a cron job to poll the bucket. That’s the serverless dream.

The event payload is wonderfully descriptive. It tells you the bucket name, the key (the object’s path), the event type, and even the entity (like a user) that caused it. But here’s the first “gotcha”: S3 event notifications are best-effort and can occasionally deliver duplicates. Your function must be idempotent. If you process image.jpg twice, your application shouldn’t fall over.

import json
import boto3

def lambda_handler(event, context):
    s3 = boto3.client('s3')
    
    for record in event['Records']:
        # Extract the bucket and key from the S3 event
        bucket = record['s3']['bucket']['name']
        key = record['s3']['object']['key']
        
        # Now go get the object and do something with it
        try:
            response = s3.get_object(Bucket=bucket, Key=key)
            data = response['Body'].read()
            print(f"Successfully processed {key} from {bucket}")
            # ... your business logic here ...
        except Exception as e:
            print(f"Error getting object {key} from bucket {bucket}. Error: {e}")
            raise e

Also, be wary of the event storm. Drop 100,000 files in there? Get ready for 100,000 Lambda invocations, all trying to happen at once. Your pocketbook and your downstream services will feel it. Sometimes an SQS queue in between is a wiser choice.

SQS: The Decoupling Workhorse

This is one of my favorites. You hook a Lambda function to an SQS queue, and it becomes a powerful, persistent, and scalable consumer. The beauty is in the mechanics: the Lambda service polls the queue on your behalf, not the other way around. It grabs a batch of messages (up to 10,000 per batch if you’re using FIFO, but let’s be real, you’re probably using Standard), and invokes your function with that batch.

The killer feature here is the built-in retry logic. If your function throws an error, the message visibility timeout expires, and the message goes right back into the queue for another try. No lost work. This is where the ReportBatchItemFailures response comes in. It’s your way of telling SQS, “Hey, these specific messages in the batch failed, but the rest are fine. Please only retry the broken ones.” It’s a fantastic way to handle partial batch failures.

def lambda_handler(event, context):
    batch_item_failures = []
    
    for record in event['Records']:
        try:
            # Process your message
            process_message(record['body'])
        except Exception as e:
            # Mark this specific message as failed
            batch_item_failures.append({"itemIdentifier": record['messageId']})
    
    # If any messages failed, tell SQS about them
    return {"batchItemFailures": batch_item_failures}

The pitfall? Visibility timeouts. If your function’s timeout is longer than the queue’s visibility timeout, you might get a duplicate message. The function is still working on the original, but SQS thinks it’s gone and dead and puts it back in the queue. Keep your function timeout shorter than the visibility timeout.

SNS: The Simple Pub/Shout

SNS is the town crier. It gets a message and shouts it to everyone who’s subscribed. When a Lambda function is a subscriber, it’s a simple fire-and-forget (from SNS’s perspective). SNS invokes your function asynchronously, meaning it doesn’t wait for a response. The event structure is also simpler; it’s just the message SNS published.

The main thing to know here is that SNS has its own retry policy with exponential backoff if your function fails. It will keep trying for a while before finally giving up and dumping the event into a Dead-Letter Queue (DLQ), if you’ve configured one. Always configure a DLQ.

DynamoDB Streams: The Database’s Memory

This is how you build reactive applications. A DynamoDB Stream is an ordered flow of item-level modifications (inserts, updates, deletes). Your Lambda function taps into this stream and can react to changes in near real-time. It’s the backbone for everything from aggregation tables to sending notifications when a user’s profile is updated.

The event payload is rich. It contains the old image and the new image of the item, so you can see exactly what changed. The eventID is your friend here for deduplication, as streams guarantee at-least-once delivery.

def lambda_handler(event, context):
    for record in event['Records']:
        # What kind of event was it?
        if record['eventName'] == 'INSERT':
            old_image = None
            new_image = record['dynamodb']['NewImage']
            # Handle the new item
        elif record['eventName'] == 'MODIFY':
            old_image = record['dynamodb']['OldImage']
            new_image = record['dynamodb']['NewImage']
            # See what specifically changed
        elif record['eventName'] == 'REMOVE':
            old_image = record['dynamodb']['OldImage']
            new_image = None
            # Handle the removal

The scaling is controlled by the number of shards in your stream, which is itself determined by the write capacity of your table. More writes, more shards, more concurrent Lambda invocations.

API Gateway: The HTTP Front Door

This is the most familiar trigger. A user hits a URL, API Gateway translates that HTTP request into a JSON event for your Lambda, and your function returns a JSON object that Gateway transforms back into an HTTP response. It’s the classic serverless web application pattern.

The big “aha!” moment for most people is understanding the proxy integration. This is where API Gateway basically hands your function the entire HTTP request, cookies, headers, path parameters, and all, as a big JSON object. Your function is responsible for parsing it and returning a very specific response format.

def lambda_handler(event, context):
    # Get the HTTP method from the event
    http_method = event['requestContext']['http']['method']
    
    if http_method == 'GET':
        # Extract a path parameter, e.g., /users/{userId}
        user_id = event['pathParameters']['userId']
        user = get_user_from_database(user_id)
        return {
            'statusCode': 200,
            'headers': {'Content-Type': 'application/json'},
            'body': json.dumps(user)
        }
    else:
        return {
            'statusCode': 405,
            'body': json.dumps('Method Not Allowed')
        }

The rough edge? Cold starts are directly visible to your end user here. That first request after a deployment or period of inactivity will be slower. You can mitigate this with Provisioned Concurrency, but it costs extra. It’s the serverless tax on low-latency requirements.

EventBridge: The Grand Orchestrator

Formerly known as CloudWatch Events, think of EventBridge as the central nervous system for your event-driven architecture. It’s an event bus. Other services (or your own custom applications) emit events to the bus, and you set up rules to route these events to targets like Lambda functions.

The power is in the flexibility. You can react to events from over 90 AWS services without them needing to know about your Lambda function. A CodePipeline fails? Send an event to the bus. A new EC2 instance is launched? Send an event. Your function just needs to know how to handle the standardized EventBridge event format.

The event pattern matching is where you get precise. You can write rules like “only invoke my function if the source is aws.health and the detail-type is AWS Health Event.” It prevents your function from being invoked for events it doesn’t care about.

The best practice? Use EventBridge Schema Discovery. It will automatically catalog all the events flowing through your bus and generate code bindings for them. It saves you from the tedious work of manually defining Python classes for every possible event payload. Trust me, use it.