AWS Ses
Send email at scale with Amazon SES (Simple Email Service). Use this skill when
You are an email operations specialist who integrates Amazon SES into projects. SES is the most cost-effective option for high-volume sending, offering raw infrastructure with full control over deliverability, reputation, and compliance. ## Key Points - Can only send to verified addresses - Daily limit of 200 emails, 1/second rate - Must request production access through AWS console - Always use configuration sets — one for transactional, one for marketing - Enable account-level suppression list to prevent sending to known bad addresses - Use dedicated IPs only if sending 100K+ emails/day consistently - Set up a custom MAIL FROM domain for full authentication alignment - Publish events to SNS for real-time processing and to Firehose for analytics - Monitor SES reputation dashboard and set CloudWatch alarms on bounce/complaint rates - Use SES v2 API — v1 is legacy - Sending without a configuration set — no tracking or suppression - Not handling SNS subscription confirmation — events silently fail ## Quick Example ```bash npm install @aws-sdk/client-sesv2 ```
skilldb get email-services-skills/AWS SesFull skill: 243 linesAWS SES Email Integration
You are an email operations specialist who integrates Amazon SES into projects. SES is the most cost-effective option for high-volume sending, offering raw infrastructure with full control over deliverability, reputation, and compliance.
Core Philosophy
Infrastructure, not abstraction
SES gives you email-sending infrastructure, not a product. You build the abstractions: template management, suppression handling, analytics, and compliance. This is its strength for teams that need full control and its weakness for teams that want a turnkey solution.
Configuration sets are non-negotiable
Every email should be sent through a configuration set. They control tracking, event publishing, suppression, and sending identity. Skipping them means flying blind.
Cost scales linearly
SES charges $0.10 per 1,000 emails. At volume, this is 10-50x cheaper than Resend, SendGrid, or Postmark. But you pay in engineering time for features those services include by default.
Setup
Install
npm install @aws-sdk/client-sesv2
Initialize
import { SESv2Client, SendEmailCommand } from '@aws-sdk/client-sesv2';
const ses = new SESv2Client({
region: process.env.AWS_REGION || 'us-east-1',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
},
});
Key Techniques
Simple send
await ses.send(new SendEmailCommand({
FromEmailAddress: 'App <noreply@yourdomain.com>',
Destination: {
ToAddresses: ['user@example.com'],
},
Content: {
Simple: {
Subject: { Data: 'Your password reset link' },
Body: {
Html: { Data: '<p>Click <a href="...">here</a></p>' },
Text: { Data: 'Reset: https://...' },
},
},
},
ConfigurationSetName: 'transactional',
EmailTags: [
{ Name: 'category', Value: 'security' },
{ Name: 'workflow', Value: 'password_reset' },
],
}));
Template send
Create templates in SES, then send with template data.
import { CreateEmailTemplateCommand } from '@aws-sdk/client-sesv2';
// Create template (one-time)
await ses.send(new CreateEmailTemplateCommand({
TemplateName: 'PasswordReset',
TemplateContent: {
Subject: 'Reset your password',
Html: '<p>Hi {{name}}, click <a href="{{resetUrl}}">here</a> to reset.</p>',
Text: 'Hi {{name}}, reset your password: {{resetUrl}}',
},
}));
// Send with template
import { SendEmailCommand } from '@aws-sdk/client-sesv2';
await ses.send(new SendEmailCommand({
FromEmailAddress: 'App <noreply@yourdomain.com>',
Destination: { ToAddresses: ['user@example.com'] },
Content: {
Template: {
TemplateName: 'PasswordReset',
TemplateData: JSON.stringify({ name: 'Alice', resetUrl: 'https://...' }),
},
},
ConfigurationSetName: 'transactional',
}));
Bulk template send
Send personalized email to many recipients efficiently.
import { SendBulkEmailCommand } from '@aws-sdk/client-sesv2';
await ses.send(new SendBulkEmailCommand({
FromEmailAddress: 'App <noreply@yourdomain.com>',
DefaultContent: {
Template: {
TemplateName: 'WeeklySummary',
TemplateData: JSON.stringify({ defaultGreeting: 'Hello' }),
},
},
BulkEmailEntries: [
{
Destination: { ToAddresses: ['user1@example.com'] },
ReplacementEmailContent: {
ReplacementTemplate: {
ReplacementTemplateData: JSON.stringify({ name: 'Alice' }),
},
},
},
// ... more entries (up to 50 per call)
],
ConfigurationSetName: 'bulk',
}));
Configuration sets
import { CreateConfigurationSetCommand } from '@aws-sdk/client-sesv2';
await ses.send(new CreateConfigurationSetCommand({
ConfigurationSetName: 'transactional',
TrackingOptions: { CustomRedirectDomain: 'track.yourdomain.com' },
DeliveryOptions: { TlsPolicy: 'REQUIRE' },
ReputationOptions: { ReputationMetricsEnabled: true },
SendingOptions: { SendingEnabled: true },
SuppressionOptions: {
SuppressedReasons: ['BOUNCE', 'COMPLAINT'],
},
}));
Event destinations (SNS, CloudWatch, Firehose, EventBridge)
import { CreateConfigurationSetEventDestinationCommand } from '@aws-sdk/client-sesv2';
await ses.send(new CreateConfigurationSetEventDestinationCommand({
ConfigurationSetName: 'transactional',
EventDestinationName: 'sns-events',
EventDestination: {
Enabled: true,
MatchingEventTypes: ['SEND', 'DELIVERY', 'BOUNCE', 'COMPLAINT', 'REJECT'],
SnsDestination: { TopicArn: 'arn:aws:sns:us-east-1:123456:ses-events' },
},
}));
Event Processing
SES publishes events to SNS, CloudWatch, Kinesis Firehose, or EventBridge.
| Event | Action |
|---|---|
DELIVERY | Mark delivered |
BOUNCE (Permanent) | Suppress address |
BOUNCE (Transient) | Log, monitor |
COMPLAINT | Suppress immediately |
REJECT | Log — SES rejected before sending |
RENDERING_FAILURE | Fix template — data mismatch |
SNS webhook handler
export async function POST(req: Request) {
const body = await req.json();
// Handle SNS subscription confirmation
if (body.Type === 'SubscriptionConfirmation') {
await fetch(body.SubscribeURL);
return new Response('OK');
}
const message = JSON.parse(body.Message);
const eventType = message.eventType;
const recipient = message.mail?.destination?.[0];
switch (eventType) {
case 'Bounce':
if (message.bounce.bounceType === 'Permanent') {
await suppressAddress(recipient, 'hard_bounce');
}
break;
case 'Complaint':
await suppressAddress(recipient, 'complaint');
break;
}
return new Response('OK');
}
Sandbox vs Production
New SES accounts start in sandbox mode:
- Can only send to verified addresses
- Daily limit of 200 emails, 1/second rate
- Must request production access through AWS console
Request production access with: sending domain verified, SPF/DKIM configured, bounce/complaint handling implemented, and a clear use case description.
Best Practices
- Always use configuration sets — one for transactional, one for marketing
- Enable account-level suppression list to prevent sending to known bad addresses
- Use dedicated IPs only if sending 100K+ emails/day consistently
- Set up a custom MAIL FROM domain for full authentication alignment
- Publish events to SNS for real-time processing and to Firehose for analytics
- Monitor SES reputation dashboard and set CloudWatch alarms on bounce/complaint rates
- Use SES v2 API — v1 is legacy
Anti-Patterns
- Sending without a configuration set — no tracking or suppression
- Not handling SNS subscription confirmation — events silently fail
- Using shared IPs for both transactional and marketing at high volume
- Ignoring the SES sandbox — production access request takes 24-48 hours
- Not implementing exponential backoff on throttling errors
- Storing AWS credentials in application code instead of using IAM roles
Install this skill directly: skilldb add email-services-skills
Related Skills
Brevo
Send transactional and marketing email with Brevo (formerly Sendinblue). Use this
Courier
Send transactional notifications including email with Courier. Use this skill when
Customerio
Send transactional and marketing email with Customer.io. Use this skill when the
Email Deliverability
Optimize email deliverability across any provider. Use this skill when the project
Loops
Send transactional and marketing email with Loops. Use this skill when the project
Mailchimp Transactional
Send transactional email with Mailchimp Transactional (formerly Mandrill). Use