StackPractices
intermediate By Mathias Paulenko

Event-Driven Serverless Functions: Lambda & EventBridge

How to design loosely coupled systems using serverless functions triggered by events from message queues, databases, and webhooks.

Topics: serverless

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:

An order service publishes an OrderPlaced event to EventBridge, which fans it out to three independent consumers: a payment Lambda, a notification Lambda, and an analytics SQS queue

Variants

PatternCouplingDurabilityBest For
Direct invocationTightNoneSimple, synchronous workflows
SQS queuesLooseHighReliable async processing, retries
EventBridgeLooseHighMulti-consumer routing, filtering
SNS topicsLooseMediumBroadcast, fan-out notifications
Kinesis streamsLooseHighReal-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:

RoleAWSAzureGCP
FunctionLambdaAzure FunctionsCloud Functions
Event busEventBridgeEvent GridEventarc
QueueSQSStorage Queues / Service BusPub/Sub
Fan-out topicSNSEvent Grid topicsPub/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: OrderPlaced beats ProcessOrder — 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 ApproximateAgeOfOldestMessage in 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: ProcessPayment expects action; PaymentRequested describes 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 (redrivePolicy on the queue) and inspect the failed payload for a schema mismatch.
  • One bad record kills the batch: return batchItemFailures with the failed messageIds 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-type values; DetailType is 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

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.