Event-Driven Serverless Functions: Lambda & EventBridge
How to design loosely coupled systems using serverless functions triggered by events from message queues, databases, and webhooks.
Overview
Event-driven architecture removes the direct HTTP calls between services: instead of calling each other, services talk through events. Say a user uploads an image — out goes an ImageUploaded event. A thumbnail generator picks it up and creates a resized version; a metadata extractor picks it up too and updates the search index. Neither service knows the other exists — only the event does.
Serverless functions fit this model well: they idle at zero cost, then spin up on their own when a burst of events lands. On AWS the usual suspects are Lambda, SQS, EventBridge, and SNS.
When to Use
Use this recipe when:
- Processing asynchronous workloads that don’t need immediate responses (image processing, report generation, email sending)
- Decoupling services so they can be deployed, scaled, and failed independently
- Fanning out one event to several consumers — each reacting differently to the same fact
- Handling traffic spikes without provisioning capacity upfront
- Reacting to changes in data (database CDC) or external systems (webhooks, file uploads)
Do not reach for event-driven when the caller needs the result synchronously, when a single consumer is the only listener forever, or when strict end-to-end ordering is a hard requirement — a plain queue or direct call will be simpler.
Solution
Lambda Triggered by SQS (Python)
import json
import boto3
def lambda_handler(event, context):
for record in event['Records']:
body = json.loads(record['body'])
order_id = body['orderId']
# Process the order asynchronously
process_order(order_id)
# SQS message is deleted automatically on successful completion
return {'statusCode': 200}
def process_order(order_id):
# Business logic: validate, charge, notify
print(f"Processing order {order_id}")
EventBridge Rule (Infrastructure as Code)
# SAM / CloudFormation
OrderPlacedRule:
Type: AWS::Events::Rule
Properties:
EventBusName: default
EventPattern:
source:
- order-service
detail-type:
- OrderPlaced
Targets:
- Arn: !GetAtt PaymentFunction.Arn
Id: payment-target
- Arn: !GetAtt NotificationFunction.Arn
Id: notification-target
Publishing Events (Node.js)
const { EventBridgeClient, PutEventsCommand } = require('@aws-sdk/client-eventbridge');
const eb = new EventBridgeClient({ region: 'us-east-1' });
async function publishOrderPlaced(order) {
await eb.send(new PutEventsCommand({
Entries: [{
Source: 'order-service',
DetailType: 'OrderPlaced',
Detail: JSON.stringify({
orderId: order.id,
amount: order.total,
customerEmail: order.email,
}),
}],
}));
}
Explanation
- Events: a fact recorded once and never edited (
OrderPlaced,ImageUploaded,PaymentReceived). The event carries state but doesn’t tell consumers what to do with it. - Event producers: whoever emits the event when something notable happens. The producer doesn’t know — or care — how many consumers listen.
- Event consumers: functions or services subscribed to specific event types. Different consumers can react to the same event in their own way.
- Event buses (EventBridge): the central router of an event-driven architecture. Each event gets matched against your rules and pushed to every matching target — producers and consumers stay decoupled.
The EventBridge rule above is the fan-out point: one OrderPlaced event routes to both the payment function and the notification function, with no code coupling between them:
Variants
| Pattern | Coupling | Durability | Best For |
|---|---|---|---|
| Direct invocation | Tight | None | Simple, synchronous workflows |
| SQS queues | Loose | High | Reliable async processing, retries |
| EventBridge | Loose | High | Multi-consumer routing, filtering |
| SNS topics | Loose | Medium | Broadcast, fan-out notifications |
| Kinesis streams | Loose | High | Real-time analytics, ordered processing |
For SQS batch tuning — batch windows, partial batch responses, and dead-letter queue wiring — see Event-Driven Lambda with SQS Triggers.
Off AWS, the same topology maps to each provider’s equivalents:
| Role | AWS | Azure | GCP |
|---|---|---|---|
| Function | Lambda | Azure Functions | Cloud Functions |
| Event bus | EventBridge | Event Grid | Eventarc |
| Queue | SQS | Storage Queues / Service Bus | Pub/Sub |
| Fan-out topic | SNS | Event Grid topics | Pub/Sub topics |
The coupling model — events, producers, consumers, dead letters — is identical; only the service names and SDK calls change.
What Works
- Design events around business facts:
OrderPlacedbeatsProcessOrder— it describes what happened, not what to do, so consumers stay free to react differently. - Self-contained events: pack the context consumers need (order ID, customer email, amount) so nobody has to call back to the producer.
- Plan for duplicates: most queues deliver at-least-once, so the same event can arrive twice. Consumers must be idempotent or deduplicate using event IDs.
- Set up dead letter queues: after a configured number of retries, failed messages move to a DLQ for inspection instead of retrying forever.
- Monitor event latency and age: stale messages signal a processing bottleneck. Alarm on
ApproximateAgeOfOldestMessagein SQS. - Keep functions single-purpose: each function does one thing well — orchestration lives in the bus rules or Step Functions, not inside the handler.
Common Mistakes
- Treating events as commands:
ProcessPaymentexpects action;PaymentRequesteddescribes a fact. Commands create tight coupling — events promote loose coupling. - Omitting schema versioning: when an event schema changes, unupdated consumers fail. Version events (
OrderPlaced-v2) or use a schema registry. - Not handling partial batch failures: with SQS batch sizes above 1, one bad message can fail the whole batch unless you return per-item failures (
batchItemFailures). - Ignoring message ordering: standard SQS queues don’t guarantee order — use FIFO or sequence numbers only when ordering is a real requirement.
- Storing shared state in the function: instances are ephemeral; state belongs in DynamoDB, S3, or the event itself.
Troubleshooting
- Messages keep retrying and never succeed: a poison message is stuck in the retry loop — check the DLQ is wired (
redrivePolicyon the queue) and inspect the failed payload for a schema mismatch. - One bad record kills the batch: return
batchItemFailureswith the failedmessageIds from the handler so only those retry — otherwise Lambda replays everything. - Event arrives but no consumer fires: the EventBridge rule pattern does not match — test the pattern in the console against the exact
source/detail-typevalues;DetailTypeis case-sensitive. - Consumers see events out of order: standard SQS and EventBridge don’t preserve order — include a sequence number or timestamp in the payload and buffer/reorder in the consumer if needed.
- Duplicate side effects (double charge, double email): at-least-once delivery — make the consumer idempotent by storing processed
event.ids before doing the side effect.
See Also
- Event-Driven Lambda with SQS Triggers — SQS batch windows, partial batch responses, DLQ wiring
- Design Event-Driven Systems — event buses, brokers, and pub/sub design patterns
- Event Sourcing Pattern — storing state as an immutable event log
- Scheduled Jobs — when a cron trigger, not an event, is the right answer
- Amazon EventBridge documentation — rules, event patterns, and quotas
- AWS Lambda SQS trigger documentation — batching, partial failures, and scaling
- Companion repo — runnable handler, SAM template, and publisher script
Frequently Asked Questions
How is event-driven different from request-response?
Request-response (HTTP REST) is synchronous: the caller waits for a result. Event-driven is asynchronous: the producer fires an event and moves on while consumers process when ready.
Can I use event-driven architecture with non-AWS providers?
Yes. Azure Functions with Event Grid, Google Cloud Functions with Pub/Sub, and Apache Kafka on any cloud all support event-driven patterns — the coupling model is the same, only the service names change.
How do I trace a request across multiple event-driven functions?
Generate a correlation ID at the entry point and propagate it through every event's attributes — X-Ray or OpenTelemetry can then stitch the chain back together.
What is the maximum event size?
Both SQS messages and EventBridge events cap at 256 KB. For bigger payloads, drop the data in S3 and send only the reference — the claim-check pattern.
Does event ordering matter in serverless functions?
Only sometimes. Most event-driven work is order-insensitive by design; where order matters (inventory, ledger), use SQS FIFO with a message group ID, or keep a sequence field and let consumers apply the latest-wins rule.
What happens if a consumer is down when the event fires?
Nothing is lost: EventBridge retries delivery (up to 185 times over 24 hours by default), and SQS holds messages until the visibility timeout expires. Configure the DLQ anyway so exhausted retries land somewhere inspectable.
Related Resources
Build Serverless APIs with API Gateway
How to design, deploy, and manage serverless HTTP APIs using AWS API Gateway, Lambda, and function-as-a-service patterns.
RecipeWebhooks
How to create and consume webhook endpoints for real-time event-driven integrations.
RecipeRun Scheduled Jobs with Serverless Functions
How to replace cron jobs with serverless scheduled functions for backups, reporting, cleanup, and periodic maintenance tasks.
RecipeDesign Event-Driven Systems with Event Buses and Brokers
How to design loosely coupled services with events, event buses, and message brokers — pub/sub patterns and event sourcing for async communication.
RecipeEvent-Driven Lambda with SQS Triggers and Batch Processing
Process SQS messages with Lambda using batch windows, partial batch responses, error handling, and dead-letter queues for resilient event-driven pipelines.
PatternEvent Sourcing Pattern
Store the state of an application as a sequence of events rather than storing only the current state. An architectural pattern for audit-friendly systems.