AWS SES
AWS Lambda
Email Automation
Serverless Computing
Cloud Services

Sending email via AWS SES within AWS Lambda function

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

Introduction

AWS Lambda and Amazon SES work well together when you need transactional email without managing a mail server. The main requirements are straightforward: SES must be configured correctly, the Lambda execution role must be allowed to send mail, and the function must call the SES API in the same region where your identity is verified.

What Must Be Ready First

Before writing code, make sure the AWS side is correct.

First, verify the sender identity in SES. Depending on your setup, this may be a single email address or an entire domain. If your account is still in the SES sandbox, you can only send to verified recipients, which surprises many first-time users.

Second, give the Lambda execution role permission to call SES. A minimal IAM policy looks like this:

json
1{
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Effect": "Allow",
6      "Action": [
7        "ses:SendEmail",
8        "ses:SendRawEmail"
9      ],
10      "Resource": "*"
11    }
12  ]
13}

Third, keep region alignment in mind. If your verified identity exists in us-east-1 but your Lambda client talks to us-west-2, the call may fail even though the code looks fine.

A Simple Lambda Example in Python

Python is a good fit here because boto3 is widely used and easy to test locally. The following handler accepts a target address and message details from the event payload.

python
1import json
2import os
3
4import boto3
5
6
7SES_REGION = os.environ.get("SES_REGION", "us-east-1")
8SOURCE_EMAIL = os.environ["SOURCE_EMAIL"]
9
10ses = boto3.client("ses", region_name=SES_REGION)
11
12
13def lambda_handler(event, context):
14    to_email = event["to"]
15    subject = event.get("subject", "Notification")
16    text_body = event.get("text", "Hello from Lambda and SES.")
17
18    response = ses.send_email(
19        Source=SOURCE_EMAIL,
20        Destination={"ToAddresses": [to_email]},
21        Message={
22            "Subject": {"Data": subject, "Charset": "UTF-8"},
23            "Body": {
24                "Text": {"Data": text_body, "Charset": "UTF-8"}
25            },
26        },
27    )
28
29    return {
30        "statusCode": 200,
31        "body": json.dumps(
32            {
33                "messageId": response["MessageId"],
34                "recipient": to_email,
35            }
36        ),
37    }

Example test event:

json
1{
2  "to": "[email protected]",
3  "subject": "Build complete",
4  "text": "The nightly job finished successfully."
5}

This is enough for plain-text transactional mail. For HTML content, you can include an Html section in the body as well.

Handling HTML and Better Configuration

Most real applications want both text and HTML. They also usually read defaults from environment variables instead of hardcoding them into the function. Here is a slightly richer version:

python
1def send_notification(to_email, subject, text_body, html_body=None):
2    body = {
3        "Text": {"Data": text_body, "Charset": "UTF-8"}
4    }
5
6    if html_body:
7        body["Html"] = {"Data": html_body, "Charset": "UTF-8"}
8
9    return ses.send_email(
10        Source=SOURCE_EMAIL,
11        Destination={"ToAddresses": [to_email]},
12        Message={
13            "Subject": {"Data": subject, "Charset": "UTF-8"},
14            "Body": body,
15        },
16    )

If you send the same style of email often, move the subject and template rendering into a small helper layer. Lambda should stay focused on orchestration rather than hand-building long HTML strings inline.

Operational Considerations

A Lambda function that sends email usually lives behind another event source, such as:

  • API Gateway for user-triggered notifications
  • EventBridge for scheduled reports
  • SQS for decoupled background email delivery
  • SNS for fan-out workflows

This architecture matters because SES is an external side effect. If retries occur, you may accidentally send duplicate messages. For important flows, include an idempotency key in your event payload and store send state in DynamoDB or another durable store.

Logging is also important. Log the SES MessageId, recipient, and a request identifier, but do not log full email bodies if they may contain sensitive data.

Local and Cloud Testing

You can test the business logic locally, but the real API call still depends on AWS credentials and SES configuration. A small local smoke test looks like this:

python
1if __name__ == "__main__":
2    event = {
3        "to": "[email protected]",
4        "subject": "Local test",
5        "text": "This is a local invocation."
6    }
7    print(lambda_handler(event, None))

In AWS, use a Lambda test event first, then inspect CloudWatch Logs for failures such as access denied, identity not verified, or sandbox restrictions.

Common Pitfalls

The most common mistake is forgetting that SES identities are regional. Verifying an address in one region does not automatically verify it everywhere.

Another frequent problem is the SES sandbox. Developers often verify the sender, deploy successfully, and then wonder why sending to a normal recipient fails. If the account is still in sandbox mode, the recipient may need verification too.

Permission errors are also common. The Lambda role needs SES send permissions, and the function needs the correct region configuration.

Finally, watch for duplicate sends during retries. Lambda is event-driven, and retries are normal. If email should be sent exactly once, design for idempotency rather than assuming a single invocation.

Summary

  • SES plus Lambda is a simple serverless pattern for transactional email.
  • You need a verified SES identity, correct IAM permissions, and matching AWS regions.
  • A small boto3 handler can send plain-text or HTML messages with little code.
  • Event-driven retries can create duplicate email unless you handle idempotency.
  • CloudWatch logs and the SES MessageId are the first things to inspect when debugging.

Related reading
Course
Beginner
27 lessons
10 hours
System Design Fundamentals

Build a strong foundation in designing scalable, reliable distributed systems.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

System Design practice on Codemia

Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.

Practice system design

All Rights Reserved.