A Solution for SES Throttling

I have written many services that make use of external APIs. One of the biggest challenges that engineers face when working with API’s is throttling backend resources to avoid hitting service limits. With Amazon SQS triggers and AWS Lambda concurrency control, we can now offload those throttling strategies. You manage the concurrency provisioning, and throttling of Lambda functions through the Reserved Concurrency parameter.

According to AWS’s documentation regarding using SQS as an event source for Lambda functions, when AWS throttles a Lambda function, messages return to the queue and become visible again. SQS and Lambda’s concurrency setting work together, resulting in messages in the queue remaining in the queue until Lambda instances are available to process them. This feature is useful. For example, a properly configured Lambda function can control the flow of requests to a upstream API with service limits.

On a recent project, I implemented this solution for a client. For this project, the upstream API was Amazon Simple Email Service (SES). Every SES account has its own set of sending limits. These limits are:

  • Sending quota - the maximum number of recipients that you can send email to in a 24-hour period.
  • Maximum send rate - the maximum number of recipients that you can send email per second.

The solution I implemented was a serverless application that allows the client to send emails at a rate much higher than the email per second-rated allowed for their SES account. The emails remain in a queue until there are Lambda instances available to process them. I have released this application as an open-source project. The source code for the project is available at https://gitlab.com/jzj/serverless-email-queue

Introduction

“Serverless Email Queue” is an application build using the AWS Serverless Application Model (SAM) framework. SAM is an open-source framework for building serverless applications. “Serverless Email Queue” provides two interfaces for sending email messages: an SQS message queue and an optional REST API. SQS sends messages to a Lambda function that forwards the messages as emails to SES. SES sends the email to its final destination.

Application Architecture

“Serverless Email Queue” makes use of both the “Scalable Webhook” and “Frugal Consumer” microservice architectures.

Sequence Diagrams

API Gateway Sequence

SQS Sequence

AWS Infrastructure Diagram

AWS Infrastructure

Build and Deployment Instructions

These instructions are for a Linux operating system. You can deploy this application from a Windows system; however, you will need to use the equivalent Windows commands:

  1. Install the SAM CLI.
  2. Install Node.JS. Release 12.20.0 was used to build this project. However, the latest recommended release should work.
  3. Clone the repository: git clone https://gitlab.com/jzj/serverless-email-queue
  4. In a terminal/console window, cd into the serverless-email-queue folder that you extracted.`
  5. In the base of the repo, run the following commands:
1
2
3
4
5
6
7
export AWS_SDK_LOAD_CONFIG=1
export SAM_CLI_TELEMETRY=0
npm install
npm run test
npm run build
sam build --base-dir ./dist
sam deploy --guided

Usage

To send messages via the REST API, you need the following:

  1. The URL of the REST API.
  2. A valid API Gateway API key.

AWS SQS API

To send messages directly to the message queue, you need the following:

  1. The SQS queue URL.
  2. Installation and configuration of the AWS SDK.
  3. The AWS IAM permissions to publish to the SQS queue URL.

REST API

Sending Email Messages

The following are the possible fields of an email message:

  • from - The email address of the sender. All email addresses can be plain ‘sender@server.com’ or formatted ‘“Sender Name” sender@server.com’.
  • to - Comma separated list or an array of recipients email addresses that will appear on the To: field.
  • subject - The subject of the email.
  • text - The plaintext version of the message as a Unicode string.
  • html - The HTML version of the message as a Unicode string.

The format of the SQS message must be a JSON encoded string. Here is an example:

1
2
3
4
{\"from\":\"foo@example.com\",
\"to\":[\"success@simulator.amazonses.com\"],
\"subject\":\"test #1\",
\"text\":\"just a test\"}

A PHP Code Example for using the REST API

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
$senderEmail = 'foo@example.com';
$recipientEmails = ['success@simulator.amazonses.com'];
$subject = 'Test Message from Serverless Email Gateway API';
$textBody = 'This email was sent with Serverless Email Gateway API.' ;
$htmlBody =  '<h1>Just a test</h1><p>This email was sent with Serverless Email Gateway API.</p>';
$apiUrl = 'https://0000000000.execute-api.us-east-1.amazonaws.com/Prod/send';
$apiKey = '0000000000000000000000000000000000000000';

foreach ($recipientEmails as $recipientEmail) {
  try {
    $message = array(
      'from' => $senderEmail,
      'to' => [$recipientEmail],
      'text' => $textBody,
      'html' => $htmlBody,
      'subject' => $subject
    );
    $messageBody = json_encode($message);
    $opts = array('http' =>
      array(
          'method'  => 'POST',
          'header'  => array('Content-Type: application/json', 'x-api-key: '.$apiKey),
          'content' => $messageBody
      )
    );
    $context = stream_context_create($opts);
    $result = file_get_contents($apiUrl, false, $context);
    if (!$result) {
      echo("Email queue via the API failed!\n".$result."\n");
      throw new ErrorException('unknown error');
    }
    echo("Email queued via the API!\n".$result."\n");
  } catch (Exception $e) {
    echo("The email was not sent via the API.\nError message: ".$e->getMessage()."\n");
    echo "\n";
  }
}

A PHP Code Example for using the SQS Queue

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
require 'vendor/autoload.php';

use Aws\Sqs\SqsClient;
use Aws\Exception\AwsException;

$client = SqsClient::factory(array(
  'profile' => 'default',
  'region'  => 'us-east-1',
  'version' => '2012-11-05'
));

$queueUrl = 'https://sqs.us-east-1.amazonaws.com/00000000000/EmailMessageQueue';
$senderEmail = 'foo@example.com';
$recipientEmails = ['success@simulator.amazonses.com'];
$subject = 'Test Message from Serverless Email Gateway';
$textBody = 'This email was sent with Serverless Email Gateway SQS.' ;
$htmlBody =  '<h1>Just a test</h1><p>This email was sent with Serverless Email Gateway SQS</p>';

foreach ($recipientEmails as $recipientEmail) {
  try {
    $message = array(
      'from' => $senderEmail,
      'to' => [$recipientEmail],
      'text' => $textBody,
      'html' => $htmlBody,
      'subject' => $subject
    );
    $messageBody = json_encode($message);
    $params = [
      'MessageBody' => $messageBody,
      'QueueUrl' => $queueUrl
    ];
    $result = $client->sendMessage($params);
    echo("Email queued!\n".$result."\n");
  } catch (AwsException $e) {
    echo $e->getMessage();
    echo("The email was not sent to the queue.\nError message: ".$e->getAwsErrorMessage()."\n");
    echo "\n";
  }
}

Summary

SES throttles API requests and will reject requests that exceed quota limits. “Serverless Email Queue” is a serverless application that automatically throttles requests to SES. Therefore, the rate for sending email messages to “Serverless Email Queue” can exceed the SES quota limits. “Serverless Email Queue” stores backlogged messages in a message queue until the sending capacity is available. This capability is useful for sending large amounts of email in a short time.

Additional Resources

Posted on:
January 5, 2021
Length:
6 minute read, 1101 words
Categories:
Serverless