Amazon SES 送信制限数を超えたときのエラーメッセージ

送信制限数に関するCloudWatchのメトリクスがないとは思っていませんでした。
2021.03.26

Amazon SESの送信制限を超えた際のエラーメッセージと、挙動を確認したかったので簡単な検証しました。

結論

  • 24時間以内の送信制限数を超過するとDaily message quota exceededエラーが返される。
  • 秒間の最大送信レートを超過するとMaximum sending rate exceededエラーが返される。
  • SESのCloudWatchメトリクスに送信制限数の項目はない。SESのダッシュボードか、AWS CLI、SDKで確認できる。
  • 送信制限に引っかからないかモニタリングしたい場合、自前でカスタムメトリクスを取得する必要がある。

Errors related to the sending quotas for your Amazon SES account - Amazon Simple Email Service

送信制限の確認

アカウントダッシュボードから送信制限値を確認できます。

また、aws sesコマンドからも送信制限値を確認できます。

$ aws ses get-send-quota
{
    "Max24HourSend": 200.0,
    "MaxSendRate": 1.0,
    "SentLast24Hours": 0.0
}

現在の送信制限値

SESはサンドボックス環境です。制限値は低い値になっています。制限値が低いため容易に超過することができるので試してみます。

項目 備考
Maximum daily sends 200通/24時間以内 制限値を超えると送信できない
Maximum sending rate 1通/秒 短時間であれば制限を超えることができる
制限を超過したまま持続することはできない

送信制限の詳細

最大送信レート

1秒に1通のメールを合計10通送信してみます。

$ go version
go version go1.16.2 darwin/amd64

main.go

package main

import (
	"fmt"
	"strconv"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ses"
)

const (
	Sender    = "[your-ses-domain]"
	Recipient = "[your-mailadress]"
	Subject   = "SES Test Mail"
	Body      = "Test mail count "
	CharSet   = "UTF-8"
)

func main() {
	sess := session.Must(session.NewSession())
	svc := ses.New(
		sess,
		aws.NewConfig().WithRegion("ap-northeast-1"))

	startFor := time.Now()
	for i := 0; i < 10; i++ {
		sendEmail(svc, i)
		time.Sleep(time.Second) // ただ1秒待つ
	}
	endFor := time.Now()
	fmt.Printf("for実行時間: %f秒\n", (endFor.Sub(startFor)).Seconds())

}

func sendEmail(svc *ses.SES, i int) {
	// Assemble the email.
	input := &ses.SendEmailInput{
		Destination: &ses.Destination{
			CcAddresses: []*string{},
			ToAddresses: []*string{
				aws.String(Recipient),
			},
		},
		Message: &ses.Message{
			Body: &ses.Body{
				Text: &ses.Content{
					Charset: aws.String(CharSet),
					Data:    aws.String(Body + strconv.Itoa(i)),
				},
			},
			Subject: &ses.Content{
				Charset: aws.String(CharSet),
				Data:    aws.String(Subject),
			},
		},
		Source: aws.String(Sender),
	}

	// Attempt to send the email.
	result, err := svc.SendEmail(input)

	// Display error messages if they occur.
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case ses.ErrCodeMessageRejected:
				fmt.Println(ses.ErrCodeMessageRejected, aerr.Error())
			case ses.ErrCodeMailFromDomainNotVerifiedException:
				fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
			case ses.ErrCodeConfigurationSetDoesNotExistException:
				fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
			default:
				fmt.Println(aerr.Error())
			}
		} else {
			// Print the error, cast err to awserr.Error to get the Code and
			// Message from an error.
			fmt.Println(err.Error())
		}

		return
	}

	fmt.Println(result)
}

実行結果

--- 省略 ---
{
  MessageId: "0106017862e482c3-4921512d-e638-4dc1-aebb-b431b58c8b2b-000000"
}
for実行時間: 12.032761秒

実行結果はすべてMessageIdが返ってきて正常に送信できました。 送信先の受信トレイを確認するとテストメールが10通届きました。

送信レートを超えて送信してみる

短時間であれば最大送信レートの1通/秒を超えることができると説明されていました。

1通送信のたびに1秒待機するのはやめ、10通のメール送信を並行処理に変更します。

main.go

package main

import (
	"fmt"
	"strconv"
	"sync"
	"time"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ses"
)

const (
	Sender    = "[your-ses-domain]"
	Recipient = "[your-mailadress]"
	Subject   = "SES Test Mail"
	Body      = "Test mail count"
	CharSet   = "UTF-8"
)

func main() {
	sess := session.Must(session.NewSession())
	svc := ses.New(
		sess,
		aws.NewConfig().WithRegion("ap-northeast-1"))

	var wg sync.WaitGroup
	startGoroutine := time.Now()
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go sendEmail(svc, i, &wg)
	}
	wg.Wait()
	endGotoutine := time.Now()
	fmt.Printf("Goroutine実行時間: %f秒\n", (endGotoutine.Sub(startGoroutine)).Seconds())

}

func sendEmail(svc *ses.SES, i int, wg *sync.WaitGroup) {
	defer wg.Done()
	// Assemble the email.
	input := &ses.SendEmailInput{
		Destination: &ses.Destination{
			CcAddresses: []*string{},
			ToAddresses: []*string{
				aws.String(Recipient),
			},
		},
		Message: &ses.Message{
			Body: &ses.Body{
				Text: &ses.Content{
					Charset: aws.String(CharSet),
					Data:    aws.String(Body + strconv.Itoa(i)),
				},
			},
			Subject: &ses.Content{
				Charset: aws.String(CharSet),
				Data:    aws.String(Subject),
			},
		},
		Source: aws.String(Sender),
	}

	// Attempt to send the email.
	result, err := svc.SendEmail(input)

	// Display error messages if they occur.
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			case ses.ErrCodeMessageRejected:
				fmt.Println(ses.ErrCodeMessageRejected, aerr.Error())
			case ses.ErrCodeMailFromDomainNotVerifiedException:
				fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
			case ses.ErrCodeConfigurationSetDoesNotExistException:
				fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
			default:
				fmt.Println(aerr.Error())
			}
		} else {
			// Print the error, cast err to awserr.Error to get the Code and
			// Message from an error.
			fmt.Println(err.Error())
		}

		return
	}

	fmt.Println(result)
}

実行結果

--- 省略 ---
{
  MessageId: "0106017862edf429-963b71e1-a405-4d0a-8fb8-f0135d1670ae-000000"
}
Goroutine実行時間: 0.577789秒

実行結果は同様にすべてMessageIdが返ってきて1秒以内に10通送信できました。テストメールを10通受信しています。約0.6秒という短時間であれば最大送信レートを超過しても送信できました。

24時間以内の最大送信数

アカウントダッシュボードから確認できます。すでに51通送信していました。

aws sesコマンドからも確認できます。

$ aws ses get-send-quota
{
    "Max24HourSend": 200.0,
    "MaxSendRate": 1.0,
    "SentLast24Hours": 51.0
}

200通以上送信してみる

24時間以内200通制限を超過してみます。すでに51通送信しているため、あと150通で最大送信数を超過できます。

Goroutineの並行処理数を10から200に変更して実行します。

実行結果

--- 省略 ---
{
  MessageId: "0106017862fe3d97-f29bf6fe-8d70-4f64-99aa-652e787d6061-000000"
}
Goroutine実行時間: 1.515848秒

実行結果はすべてMessageIdが返ってきました。最大送信数と最大送信レートに引っかかることなく約1.5秒で200通送信できました。201通目からエラーになると想定していたのですが本当に送信できたのでしょうか。

ダッシュボードから確認すると大幅に超過しています。

$  aws ses get-send-quota
{
    "Max24HourSend": 200.0,
    "MaxSendRate": 1.0,
    "SentLast24Hours": 271.0
}

テストメールを200通受信しています。すべて受信するまでには時間がかかりました。

CloudWatchでSendのメトリクスを確認しました。10分弱レートの値が1を記録しています。最大送信レートはこの1を指しているのではないかと疑問を持ちます。

調べてみると最大送信レートより早くSESを呼び出すとMaximum sending rate exceededが発生すると説明されていました。SESから送信されるメールの送信レートではありませんでした。

脱線してしまいました。話を戻します。

制限値を超過した状態からaws sesコマンドで1通メールを送信してみました。Daily message quota exceeded.のエラーが返ってきました。

短時間で大量に送信し制限値を超えても急にメール送信停止にはなりませんでした。しかし、制限値を超えたことを確認した後に送信したくてもメール送信はできませんでした。

$ aws ses send-email \
  --from [your-ses-domain] \
  --to [your-mailadress] \
  --subject test-subject \
  --text test-body

実行結果

An error occurred (Throttling) when calling the SendEmail operation (reached max retries: 4): Daily message quota exceeded.

エラーメッセージまとめ

送信制限を超過時のエラーは下記の2種類あります。

エラーメッセージ 説明
Daily message quota exceeded 24時間以内の最大送信数超過
Maximum sending rate exceeded 秒間の最大送信レート超過

送信制限のエラー詳細

最大送信レート超過エラーも確認したい

秒間100通の最大瞬間風速的なアプローチではエラーは記録されませんでした。検証のため24時間空けて送信数を0通に戻しました。あらためて2通/秒のペースで1000通送信続けるアプローチを取ってみました。

実行結果は懐が深いようで1000通エラーなく処理されました。

1通も漏れずに1000通受信しています。

2通送信の並行処理に2秒かかることも稀にありました。反省点は安定して2通/秒をだせなかったことです。残念ですが時間の関係で打ち切ります。

送信制限のモニタリング

CloudWatchからSESのメトリクスを確認します。SendQuotaにあたる項目がありません。

Amazon SESコンソールか、Amazon SES APIで送信制限をモニタリングできますとドキュメントに書いてあります。CloudWatchでとは書いてないですね、そうでしたか。

取得してみた例

GoのSDKを使い24時間以内のメール送信数を取得してみます。

main.go

package main

import (
	"fmt"
	"log"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/awserr"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/ses"
)

func main() {
	sess := session.Must(session.NewSession())
	svc := ses.New(
		sess,
		aws.NewConfig().WithRegion("ap-northeast-1"))

	input := &ses.GetSendQuotaInput{}

	result, err := svc.GetSendQuota(input)
	if err != nil {
		if aerr, ok := err.(awserr.Error); ok {
			switch aerr.Code() {
			default:
				log.Fatal(aerr.Error())
			}
		} else {
			// Print the error, cast err to awserr.Error to get the Code and
			// Message from an error.
			log.Fatal(err.Error())
		}
		return
	}

	fmt.Printf("%g", *result.SentLast24Hours)
}

実行結果

271

ダッシュボードで確認した値と同じ値を取得できました。モニタリングするならCloudWatchへカスタムメトリクスとして定期的に送るよう仕込む必要があります。最大送信レートのモニタリングはエラー返ってきた時にアラート上げるしか方法が思いつかないです。なにかよい方法あればぜひ教えていただきたいです。

MackerelにはSent Last 24 Hoursの項目がありました。24時間以内の送信数を取得しています。

プラグインを確認するとGetSendQuotaで値取ってきているので上に載せたコードと同じ値を拾ってます。洗練されたコードを確認されたい方はMackerelエージェントのSESプラグインをご確認ください。

おわりに

最大送信レートは短時間であれば許されること知りました。あと、バウンス、苦情のメトリクスに目がいきがちで送信制限に関するメトリクスがないことを知りませんでした。ドキュメントには送信制限に近づいていないか確認することを推奨しますと書いてあるからCloudWatchの項目に追加されないかな、追加されたら嬉しいな。

参考