ワンクリックで
aws-ses-best-practices
Email deliverability, warming, compliance, and operational guidelines for AWS Simple Email Service.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
メニュー
Email deliverability, warming, compliance, and operational guidelines for AWS Simple Email Service.
Codex または Claude でインストール この Prompt をコピーして Codex、Claude、または他のアシスタントに貼り付けると、Skill ページを確認してインストールできます。
SOC 職業分類に基づく
CLI that deploys SES, DynamoDB, Lambda, and IAM resources to your AWS account with automatic DKIM, SPF, DMARC configuration.
Best practices for email content that avoids spam filters. Apply these rules when writing email copy, building templates, or debugging spam placement issues.
Interactive SES deliverability troubleshooting. Diagnoses bounces, spam placement, send failures, account health, and complaint issues with targeted AWS CLI commands and ordered remediation steps.
TypeScript SDK for AWS SES with automatic credential resolution, React.email support, and template management.
Get started with Wraps email infrastructure. Guides through deploying infrastructure, verifying a domain, installing the SDK, and sending the first email. Adapts to project framework and hosting provider.
TypeScript SDK for AWS End User Messaging (Pinpoint SMS) with opt-out management, batch sending, and E.164 validation.
| name | aws-ses-best-practices |
| description | Email deliverability, warming, compliance, and operational guidelines for AWS Simple Email Service. |
Email deliverability, warming, compliance, and operational guidelines for AWS Simple Email Service.
Add to your domain's DNS:
v=spf1 include:amazonses.com ~all
For custom MAIL FROM domain:
# TXT record for mail.yourapp.com
v=spf1 include:amazonses.com ~all
SES provides three CNAME records for DKIM. Add all three:
# Example (actual values from SES console)
selector1._domainkey.yourapp.com CNAME selector1.dkim.amazonses.com
selector2._domainkey.yourapp.com CNAME selector2.dkim.amazonses.com
selector3._domainkey.yourapp.com CNAME selector3.dkim.amazonses.com
Add TXT record to your domain:
# Start with monitoring mode
_dmarc.yourapp.com TXT "v=DMARC1; p=none; rua=mailto:dmarc@yourapp.com"
# After monitoring, move to quarantine
_dmarc.yourapp.com TXT "v=DMARC1; p=quarantine; rua=mailto:dmarc@yourapp.com"
# Production: reject unauthenticated emails
_dmarc.yourapp.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@yourapp.com"
Improves deliverability by using your domain instead of amazonses.com:
# MX record for mail.yourapp.com
mail.yourapp.com MX 10 feedback-smtp.us-east-1.amazonses.com
# SPF record for mail.yourapp.com
mail.yourapp.com TXT "v=spf1 include:amazonses.com ~all"
New SES accounts have limited sending reputation. Warm up gradually.
| Day | Daily Volume | Notes |
|---|---|---|
| 1-2 | 200 | Start small, monitor bounces |
| 3-4 | 500 | Check complaint rate |
| 5-7 | 1,000 | Monitor reputation dashboard |
| 8-14 | 5,000 | Steady increase |
| 15-21 | 10,000 | |
| 22-30 | 25,000 | |
| 30+ | 50,000+ | Scale as needed |
Hard Bounces (Permanent):
Soft Bounces (Temporary):
Transient Bounces:
| Rate | Status | Action |
|---|---|---|
| < 2% | Healthy | Continue normally |
| 2-5% | Warning | Investigate, clean list |
| 5-10% | Critical | Stop sends, clean list aggressively |
| > 10% | Danger | SES may suspend account |
// Wraps CLI sets up automatic bounce handling via SNS → Lambda → DynamoDB
// Query bounced addresses:
import { DynamoDBClient, QueryCommand } from '@aws-sdk/client-dynamodb';
const client = new DynamoDBClient({});
const bounces = await client.send(new QueryCommand({
TableName: 'wraps-email-events',
KeyConditionExpression: 'pk = :pk',
ExpressionAttributeValues: {
':pk': { S: 'BOUNCE#hard' },
},
}));
| Rate | Status | Action |
|---|---|---|
| < 0.1% | Healthy | Continue normally |
| 0.1-0.3% | Warning | Review content, add unsubscribe |
| 0.3-0.5% | Critical | Pause marketing emails |
| > 0.5% | Danger | SES may suspend account |
Add to all marketing emails:
// With raw email or custom headers
const headers = {
'List-Unsubscribe': '<mailto:unsubscribe@yourapp.com>, <https://yourapp.com/unsubscribe>',
'List-Unsubscribe-Post': 'List-Unsubscribe=One-Click',
};
Validate emails at signup:
// Basic format validation
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
// Check for common typos
const typoSuggestions: Record<string, string> = {
'gmial.com': 'gmail.com',
'gmal.com': 'gmail.com',
'hotmal.com': 'hotmail.com',
'yaho.com': 'yahoo.com',
};
// Use email verification service for important signups
// (e.g., ZeroBounce, NeverBounce, Hunter)
Remove or suppress addresses based on engagement:
| Last Engagement | Action |
|---|---|
| < 30 days | Active, send normally |
| 30-90 days | Reduce frequency |
| 90-180 days | Re-engagement campaign |
| 180+ days | Suppress from regular sends |
| 365+ days | Remove from list |
Maintain lists of addresses to never email:
// Hard bounces - permanent suppression
// Complaints - permanent suppression
// Unsubscribes - honor indefinitely
// Role addresses - suppress (admin@, info@, support@)
Words to avoid:
Formatting to avoid:
// Send at optimal local time
const sendAtLocalTime = (email: string, preferredHour: number) => {
const userTimezone = getUserTimezone(email); // from user preferences
const now = new Date();
const targetTime = new Date(now.toLocaleString('en-US', { timeZone: userTimezone }));
targetTime.setHours(preferredHour, 0, 0, 0);
if (targetTime < now) {
targetTime.setDate(targetTime.getDate() + 1);
}
return targetTime;
};
// Don't send 100k emails at once
const BATCH_SIZE = 1000;
const DELAY_BETWEEN_BATCHES_MS = 60000; // 1 minute
async function sendBulk(recipients: string[], template: string) {
for (let i = 0; i < recipients.length; i += BATCH_SIZE) {
const batch = recipients.slice(i, i + BATCH_SIZE);
await email.sendBulkTemplate({
from: 'hello@yourapp.com',
template,
destinations: batch.map(to => ({ to, templateData: {} })),
});
if (i + BATCH_SIZE < recipients.length) {
await sleep(DELAY_BETWEEN_BATCHES_MS);
}
}
}
| Metric | Target | Alert Threshold |
|---|---|---|
| Bounce Rate | < 2% | > 5% |
| Complaint Rate | < 0.1% | > 0.3% |
| Delivery Rate | > 95% | < 90% |
| Open Rate | > 20% | < 10% |
| Click Rate | > 2% | < 1% |
Wraps CLI sets up basic alarms. Add custom ones:
// High bounce rate alarm
const alarm = new cloudwatch.Alarm(this, 'HighBounceRate', {
metric: new cloudwatch.Metric({
namespace: 'AWS/SES',
metricName: 'Bounce',
statistic: 'Sum',
period: Duration.hours(1),
}),
threshold: 50,
evaluationPeriods: 1,
comparisonOperator: cloudwatch.ComparisonOperator.GREATER_THAN_THRESHOLD,
});
Check regularly in AWS Console → SES → Reputation Metrics:
<footer style="font-size: 12px; color: #666;">
<p>
You're receiving this email because you signed up at yourapp.com.
</p>
<p>
<a href="{{unsubscribe_url}}">Unsubscribe</a> |
<a href="{{preferences_url}}">Email Preferences</a>
</p>
<p>
Your Company, Inc.<br>
123 Main Street<br>
City, State 12345
</p>
</footer>
# Get sending quota and daily usage
aws ses get-send-quota
# Check if in sandbox (SESv2)
aws sesv2 get-account
# Account-level suppression settings
aws sesv2 get-account --query 'SuppressionAttributes'
# List verified domains
aws ses list-identities --identity-type Domain
# List verified email addresses
aws ses list-identities --identity-type EmailAddress
# Check verification status for a domain
aws ses get-identity-verification-attributes \
--identities yourapp.com
# Full identity details (SESv2)
aws sesv2 get-email-identity --email-identity yourapp.com
# Check DKIM configuration
aws ses get-identity-dkim-attributes --identities yourapp.com
# SESv2 DKIM details
aws sesv2 get-email-identity --email-identity yourapp.com \
--query 'DkimAttributes'
# Check custom MAIL FROM domain
aws ses get-identity-mail-from-domain-attributes \
--identities yourapp.com
# List all configuration sets
aws sesv2 list-configuration-sets
# Get details for a configuration set
aws sesv2 get-configuration-set \
--configuration-set-name my-config-set
# List event destinations
aws sesv2 get-configuration-set-event-destinations \
--configuration-set-name my-config-set
# Basic send stats (last 2 weeks, 15-min intervals)
aws ses get-send-statistics
# CloudWatch metrics for sends
aws cloudwatch get-metric-statistics \
--namespace AWS/SES \
--metric-name Send \
--start-time $(date -u -d '24 hours ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 3600 \
--statistics Sum
# Bounce rate (last 7 days)
aws cloudwatch get-metric-statistics \
--namespace AWS/SES \
--metric-name Reputation.BounceRate \
--start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 86400 \
--statistics Average
# Complaint rate (last 7 days)
aws cloudwatch get-metric-statistics \
--namespace AWS/SES \
--metric-name Reputation.ComplaintRate \
--start-time $(date -u -d '7 days ago' +%Y-%m-%dT%H:%M:%SZ) \
--end-time $(date -u +%Y-%m-%dT%H:%M:%SZ) \
--period 86400 \
--statistics Average
# List suppressed addresses
aws sesv2 list-suppressed-destinations
# Filter by reason
aws sesv2 list-suppressed-destinations --reasons BOUNCE
aws sesv2 list-suppressed-destinations --reasons COMPLAINT
# Check specific address
aws sesv2 get-suppressed-destination \
--email-address user@example.com
# Remove from suppression list
aws sesv2 delete-suppressed-destination \
--email-address user@example.com
# Add to suppression list
aws sesv2 put-suppressed-destination \
--email-address user@example.com \
--reason COMPLAINT
# Send test email
aws ses send-email \
--from verified@yourapp.com \
--to recipient@example.com \
--subject "Test Email" \
--text "This is a test email sent via AWS CLI."
# Verify email address (sandbox mode)
aws ses verify-email-identity --email-address test@example.com
# Check DKIM records
dig +short TXT selector1._domainkey.yourapp.com
# Check SPF record
dig +short TXT yourapp.com | grep spf
# Check DMARC record
dig +short TXT _dmarc.yourapp.com
# Check MX record (for custom MAIL FROM)
dig +short MX mail.yourapp.com