Envío de correos electrónicos de forma asíncrona a través de AWS SES

Publicado: 2022-03-10
Resumen rápido ↬ Enviar muchos correos electrónicos transaccionales a la vez, si no se diseña correctamente, podría convertirse en un cuello de botella para la aplicación y degradar la experiencia del usuario. Parte del problema es conectarse al servidor SMTP desde dentro de la aplicación, sincrónicamente. En este artículo, exploraremos cómo enviar correos electrónicos desde fuera de la aplicación, de forma asíncrona, utilizando una combinación de AWS S3, Lambda y SES.

La mayoría de las aplicaciones envían correos electrónicos para comunicarse con sus usuarios. Los correos electrónicos transaccionales son aquellos activados por la interacción del usuario con la aplicación, como cuando se le da la bienvenida a un nuevo usuario después de registrarse en el sitio, se le da al usuario un enlace para restablecer la contraseña o se adjunta una factura después de que el usuario realiza una compra. Todos estos casos anteriores normalmente requerirán enviar solo un correo electrónico al usuario. Sin embargo, en algunos otros casos, la aplicación necesita enviar muchos más correos electrónicos, como cuando un usuario publica contenido nuevo en el sitio, y todos sus seguidores (que, en una plataforma como Twitter, pueden sumar millones de usuarios) recibirán un notificación. En esta última situación, si no se diseña correctamente, el envío de correos electrónicos puede convertirse en un cuello de botella en la aplicación.

Eso es lo que pasó en mi caso. Tengo un sitio que puede necesitar enviar 20 correos electrónicos después de algunas acciones desencadenadas por el usuario (como notificaciones de usuario a todos sus seguidores). Inicialmente, dependía del envío de correos electrónicos a través de un popular proveedor de SMTP basado en la nube (como SendGrid, Mandrill, Mailjet y Mailgun); sin embargo, la respuesta al usuario demoraba unos segundos. Evidentemente, conectarse al servidor SMTP para enviar esos 20 correos electrónicos estaba ralentizando significativamente el proceso.

Después de la inspección, descubrí las fuentes del problema:

  1. Conexión síncrona
    La aplicación se conecta al servidor SMTP y espera un reconocimiento, de forma síncrona, antes de continuar con la ejecución del proceso.
  2. Alta latencia
    Si bien mi servidor está ubicado en Singapur, el proveedor SMTP que estaba usando tiene sus servidores ubicados en los EE. UU., lo que hace que la conexión de ida y vuelta tome un tiempo considerable.
  3. Sin reutilización de la conexión SMTP
    Al llamar a la función para enviar un correo electrónico, la función envía el correo electrónico inmediatamente, creando una nueva conexión SMTP en ese momento (no ofrece recopilar todos los correos electrónicos y enviarlos todos juntos al final de la solicitud, bajo un solo SMTP conexión).

Debido a # 1, el tiempo que el usuario debe esperar la respuesta está vinculado al tiempo que lleva enviar los correos electrónicos. Debido a #2, el tiempo para enviar un correo electrónico es relativamente alto. Y debido al #3, el tiempo para enviar 20 correos electrónicos es 20 veces el tiempo que se tarda en enviar un correo electrónico. Si bien es posible que enviar un solo correo electrónico no haga que la aplicación sea terriblemente más lenta, enviar 20 correos electrónicos ciertamente lo hace, lo que afecta la experiencia del usuario.

Veamos cómo podemos resolver este problema.

¡Más después del salto! Continúe leyendo a continuación ↓

Prestando atención a la naturaleza de los correos electrónicos transaccionales

Antes que nada, debemos notar que no todos los correos electrónicos tienen la misma importancia. En términos generales, podemos clasificar los correos electrónicos en dos grupos: correos electrónicos prioritarios y no prioritarios. Por ejemplo, si el usuario olvidó la contraseña para acceder a la cuenta, esperará el correo electrónico con el enlace para restablecer la contraseña inmediatamente en su bandeja de entrada; ese es un correo prioritario. Por el contrario, enviar un correo electrónico notificando que alguien a quien seguimos ha publicado contenido nuevo no tiene por qué llegar a la bandeja de entrada del usuario de inmediato; ese es un correo no prioritario.

La solución debe optimizar cómo se envían estas dos categorías de correos electrónicos. Suponiendo que solo se enviarán unos pocos (quizás 1 o 2) correos electrónicos prioritarios durante el proceso, y la mayor parte de los correos electrónicos no serán prioritarios, entonces diseñamos la solución de la siguiente manera:

  • Los correos electrónicos prioritarios pueden simplemente evitar el problema de alta latencia mediante el uso de un proveedor SMTP ubicado en la misma región donde se implementa la aplicación. Además de una buena investigación, esto implica integrar nuestra aplicación con la API del proveedor.
  • Los correos electrónicos no prioritarios se pueden enviar de forma asíncrona y en lotes donde muchos correos electrónicos se envían juntos. Implementado a nivel de aplicación, requiere una pila de tecnología apropiada.

A continuación, definamos la pila de tecnología para enviar correos electrónicos de forma asíncrona.

Definición de la pila de tecnología

Nota: he decidido basar mi pila en los servicios de AWS porque mi sitio web ya está alojado en AWS EC2. De lo contrario, tendría una sobrecarga por mover datos entre las redes de varias empresas. Sin embargo, también podemos implementar nuestra solución utilizando otros proveedores de servicios en la nube.

Mi primer enfoque fue establecer una cola. A través de una cola, podría hacer que la aplicación ya no envíe los correos electrónicos, sino que publique un mensaje con el contenido del correo electrónico y los metadatos en una cola, y luego haga que otro proceso recoja los mensajes de la cola y envíe los correos electrónicos.

Sin embargo, al revisar el servicio de colas de AWS, llamado SQS, decidí que no era una solución adecuada, porque:

  • Es bastante complejo de configurar;
  • Un mensaje de cola estándar puede almacenar solo hasta 256 kb de información, lo que puede no ser suficiente si el correo electrónico tiene archivos adjuntos (una factura, por ejemplo). Y aunque es posible dividir un mensaje grande en mensajes más pequeños, la complejidad crece aún más.

Luego me di cuenta de que podía imitar perfectamente el comportamiento de una cola mediante una combinación de otros servicios de AWS, S3 y Lambda, que son mucho más fáciles de configurar. S3, una solución de almacenamiento de objetos en la nube para almacenar y recuperar datos, puede actuar como repositorio para cargar los mensajes, y Lambda, un servicio informático que ejecuta código en respuesta a eventos, puede seleccionar un mensaje y ejecutar una operación con él.

En otras palabras, podemos configurar nuestro proceso de envío de correo electrónico de esta manera:

  1. La aplicación carga un archivo con el contenido del correo electrónico y los metadatos en un depósito de S3.
  2. Cada vez que se carga un archivo nuevo en el depósito de S3, S3 activa un evento que contiene la ruta al archivo nuevo.
  3. Una función de Lambda selecciona el evento, lee el archivo y envía el correo electrónico.

Finalmente, tenemos que decidir cómo enviar correos electrónicos. Podemos seguir usando el proveedor SMTP que ya tenemos, haciendo que la función Lambda interactúe con sus API, o usar el servicio de envío de correos de AWS, llamado SES. El uso de SES tiene ventajas y desventajas:

Beneficios:

  • Muy simple de usar desde AWS Lambda (solo se necesitan 2 líneas de código).
  • Es más económico: las tarifas de Lambda se calculan en función de la cantidad de tiempo que lleva ejecutar la función, por lo que conectarse a SES desde la red de AWS llevará menos tiempo que conectarse a un servidor externo, lo que hace que la función finalice antes y cueste menos. . (A menos que SES no esté disponible en la misma región donde se aloja la aplicación; en mi caso, debido a que SES no se ofrece en la región del Pacífico asiático (Singapur), donde se encuentra mi servidor EC2, sería mejor que me conectara a algún proveedor de SMTP externo con sede en Asia).

Inconvenientes:

  • No se proporcionan muchas estadísticas para monitorear nuestros correos electrónicos enviados, y agregar otras más potentes requiere un esfuerzo adicional (por ejemplo, el seguimiento del porcentaje de correos electrónicos que se abrieron o en qué enlaces se hizo clic, debe configurarse a través de AWS CloudWatch).
  • Si seguimos usando el proveedor SMTP para enviar los correos electrónicos prioritarios, entonces no tendremos todas nuestras estadísticas juntas en un solo lugar.

Para simplificar, en el siguiente código usaremos SES.

Luego, hemos definido la lógica del proceso y la pila de la siguiente manera: la aplicación envía correos electrónicos prioritarios como de costumbre, pero para los que no son prioritarios, carga un archivo con el contenido del correo electrónico y los metadatos en S3; este archivo se procesa de forma asíncrona mediante una función Lambda, que se conecta a SES para enviar el correo electrónico.

Comencemos a implementar la solución.

Diferenciar entre correos electrónicos prioritarios y no prioritarios

En resumen, todo esto depende de la aplicación, por lo que debemos decidir correo por correo electrónico. Describiré una solución que implementé para WordPress, que requiere algunos trucos en torno a las restricciones de la función wp_mail . Para otras plataformas, la estrategia a continuación también funcionará, pero muy posiblemente habrá mejores estrategias, que no requieren hacks para funcionar.

La forma de enviar un correo electrónico en WordPress es llamando a la función wp_mail , y no queremos cambiar eso (por ejemplo, llamando a la función wp_mail_synchronous o wp_mail_asynchronous ), por lo que nuestra implementación de wp_mail necesitará manejar casos sincrónicos y asincrónicos. y necesitará saber a qué grupo pertenece el correo electrónico. Desafortunadamente, wp_mail no ofrece ningún parámetro extra desde el cual podamos evaluar esta información, como se puede ver en su firma:

 function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() )

Luego, para averiguar la categoría del correo electrónico, agregamos una solución hacky: por defecto, hacemos que un correo electrónico pertenezca al grupo de prioridad, y si $to contiene un correo electrónico en particular (por ejemplo: [email protected]), o si $subject comienza con una cadena especial (p. ej.: “[¡No prioritario!]“), entonces pertenece al grupo no prioritario (y eliminamos el correo electrónico o la cadena correspondiente del asunto). wp_mail es una función conectable, por lo que podemos anularla simplemente implementando una nueva función con la misma firma en nuestro archivo functions.php. Inicialmente, contiene el mismo código de la función wp_mail original, ubicada en el archivo wp-includes/pluggable.php, para extraer todos los parámetros:

 if ( !function_exists( 'wp_mail' ) ) : function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) { $atts = apply_filters( 'wp_mail', compact( 'to', 'subject', 'message', 'headers', 'attachments' ) ); if ( isset( $atts['to'] ) ) { $to = $atts['to']; } if ( !is_array( $to ) ) { $to = explode( ',', $to ); } if ( isset( $atts['subject'] ) ) { $subject = $atts['subject']; } if ( isset( $atts['message'] ) ) { $message = $atts['message']; } if ( isset( $atts['headers'] ) ) { $headers = $atts['headers']; } if ( isset( $atts['attachments'] ) ) { $attachments = $atts['attachments']; } if ( ! is_array( $attachments ) ) { $attachments = explode( "\n", str_replace( "\r\n", "\n", $attachments ) ); } // Continue below... } endif;

Y luego verificamos si no es prioritario, en cuyo caso nos bifurcamos en una lógica separada bajo la función send_asynchronous_mail o, si no lo es, seguimos ejecutando el mismo código que en la función wp_mail original:

 function wp_mail( $to, $subject, $message, $headers = '', $attachments = array() ) { // Continued from above... $hacky_email = "[email protected]"; if (in_array($hacky_email, $to)) { // Remove the hacky email from $to array_splice($to, array_search($hacky_email, $to), 1); // Fork to asynchronous logic return send_asynchronous_mail($to, $subject, $message, $headers, $attachments); } // Continue all code from original function in wp-includes/pluggable.php // ... }

En nuestra función send_asynchronous_mail , en lugar de cargar el correo electrónico directamente a S3, simplemente agregamos el correo electrónico a una variable global $emailqueue , desde la cual podemos cargar todos los correos electrónicos juntos a S3 en una sola conexión al final de la solicitud:

 function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { global $emailqueue; if (!$emailqueue) { $emailqueue = array(); } // Add email to queue. Code continues below... }

Podemos cargar un archivo por correo electrónico, o podemos agruparlos para que en 1 archivo contengamos muchos correos electrónicos. Dado que $headers contiene metadatos de correo electrónico (de, tipo de contenido y juego de caracteres, CC, BCC y campos de respuesta), podemos agrupar los correos electrónicos siempre que tengan los mismos $headers . De esta forma, todos estos correos electrónicos se pueden cargar en el mismo archivo a S3, y la metainformación $headers se incluirá solo una vez en el archivo, en lugar de una vez por correo electrónico:

 function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { // Continued from above... // Add email to the queue $emailqueue[$headers] = $emailqueue[$headers] ?? array(); $emailqueue[$headers][] = array( 'to' => $to, 'subject' => $subject, 'message' => $message, 'attachments' => $attachments, ); // Code continues below }

Finalmente, la función send_asynchronous_mail devuelve true . Tenga en cuenta que este código es pirateado: true normalmente significa que el correo electrónico se envió correctamente, pero en este caso, aún no se ha enviado y podría fallar perfectamente. Debido a esto, la función que llama a wp_mail no debe tratar una respuesta true como "el correo electrónico se envió con éxito", sino como un reconocimiento de que se ha puesto en cola. Por eso es importante restringir esta técnica a correos electrónicos no prioritarios para que, si falla, el proceso pueda volver a intentarlo en segundo plano y el usuario no espere que el correo electrónico ya esté en su bandeja de entrada:

 function send_asynchronous_mail($to, $subject, $message, $headers, $attachments) { // Continued from above... // That's it! return true; }

Carga de correos electrónicos en S3

En mi artículo anterior "Compartir datos entre varios servidores a través de AWS S3", describí cómo crear un depósito en S3 y cómo cargar archivos en el depósito a través del SDK. Todo el código a continuación continúa con la implementación de una solución para WordPress, por lo tanto, nos conectamos a AWS usando el SDK para PHP.

Podemos extendernos desde la clase abstracta AWS_S3 (presentada en mi artículo anterior) para conectarnos a S3 y cargar los correos electrónicos en un depósito "async-emails" al final de la solicitud (activado a través wp_footer ). Tenga en cuenta que debemos mantener la ACL como "privada" ya que no queremos que los correos electrónicos estén expuestos a Internet:

 class AsyncEmails_AWS_S3 extends AWS_S3 { function __construct() { // Send all emails at the end of the execution add_action("wp_footer", array($this, "upload_emails_to_s3"), PHP_INT_MAX); } protected function get_acl() { return "private"; } protected function get_bucket() { return "async-emails"; } function upload_emails_to_s3() { $s3Client = $this->get_s3_client(); // Code continued below... } } new AsyncEmails_AWS_S3();

Comenzamos iterando a través de los pares de encabezados => datos de correo electrónico guardados en la variable global $emailqueue y obtenemos una configuración predeterminada de la función get_default_email_meta si los encabezados están vacíos. En el siguiente código, solo recupero el campo "de" de los encabezados (el código para extraer todos los encabezados se puede copiar de la función original wp_mail ):

 class AsyncEmails_AWS_S3 extends AWS_S3 { public function get_default_email_meta() { // Code continued from above... return array( 'from' => sprintf( '%s <%s>', get_bloginfo('name'), get_bloginfo('admin_email') ), 'contentType' => 'text/html', 'charset' => strtolower(get_option('blog_charset')) ); } public function upload_emails_to_s3() { // Code continued from above... global $emailqueue; foreach ($emailqueue as $headers => $emails) { $meta = $this->get_default_email_meta(); // Retrieve the "from" from the headers $regexp = '/From:\s*(([^\<]*?) <)? ?\s*\n/i'; if(preg_match($regexp, $headers, $matches)) { $meta['from'] = sprintf( '%s <%s>', $matches[2], $matches[3] ); } // Code continued below... } } } class AsyncEmails_AWS_S3 extends AWS_S3 { public function get_default_email_meta() { // Code continued from above... return array( 'from' => sprintf( '%s <%s>', get_bloginfo('name'), get_bloginfo('admin_email') ), 'contentType' => 'text/html', 'charset' => strtolower(get_option('blog_charset')) ); } public function upload_emails_to_s3() { // Code continued from above... global $emailqueue; foreach ($emailqueue as $headers => $emails) { $meta = $this->get_default_email_meta(); // Retrieve the "from" from the headers $regexp = '/From:\s*(([^\<]*?) <)? ?\s*\n/i'; if(preg_match($regexp, $headers, $matches)) { $meta['from'] = sprintf( '%s <%s>', $matches[2], $matches[3] ); } // Code continued below... } } }

Finalmente, subimos los correos electrónicos a S3. Decidimos cuántos correos electrónicos subir por archivo con la intención de ahorrar dinero. Las funciones Lambda se cobran según la cantidad de tiempo que necesitan para ejecutarse, calculado en lapsos de 100 ms. Cuanto más tiempo requiere una función, más costosa se vuelve.

Entonces, enviar todos los correos electrónicos cargando 1 archivo por correo electrónico es más costoso que cargar 1 archivo por muchos correos electrónicos, ya que la sobrecarga de ejecutar la función se calcula una vez por correo electrónico, en lugar de solo una vez para muchos correos electrónicos, y también porque enviar muchos correos electrónicos juntos llena los intervalos de 100 ms más a fondo.

Así que subimos muchos correos electrónicos por archivo. ¿Cuántos correos electrónicos? Las funciones de Lambda tienen un tiempo de ejecución máximo (3 segundos de forma predeterminada) y, si la operación falla, seguirá intentándolo desde el principio, no desde donde falló. Por lo tanto, si el archivo contiene 100 correos electrónicos y Lambda logra enviar 50 correos electrónicos antes de que se agote el tiempo máximo, falla y vuelve a intentar ejecutar la operación, enviando los primeros 50 correos electrónicos una vez más. Para evitar esto, debemos elegir una cantidad de correos electrónicos por archivo que estemos seguros de que es suficiente para procesar antes de que se agote el tiempo máximo. En nuestra situación, podríamos optar por enviar 25 correos electrónicos por archivo. El número de correos electrónicos depende de la aplicación (los correos electrónicos más grandes tardarán más en enviarse y el tiempo para enviar un correo electrónico dependerá de la infraestructura), por lo que debemos hacer algunas pruebas para llegar al número correcto.

El contenido del archivo es simplemente un objeto JSON, que contiene el meta del correo electrónico en la propiedad "meta", y la parte de los correos electrónicos en la propiedad "correos electrónicos":

 class AsyncEmails_AWS_S3 extends AWS_S3 { public function upload_emails_to_s3() { // Code continued from above... foreach ($emailqueue as $headers => $emails) { // Code continued from above... // Split the emails into chunks of no more than the value of constant EMAILS_PER_FILE: $chunks = array_chunk($emails, EMAILS_PER_FILE); $filename = time().rand(); for ($chunk_count = 0; $chunk_count < count($chunks); $chunk_count++) { $body = array( 'meta' => $meta, 'emails' => $chunks[$chunk_count], ); // Upload to S3 $s3Client->putObject([ 'ACL' => $this->get_acl(), 'Bucket' => $this->get_bucket(), 'Key' => $filename.$chunk_count.'.json', 'Body' => json_encode($body), ]); } } } }

Para simplificar, en el código anterior, no estoy cargando los archivos adjuntos a S3. Si nuestros correos electrónicos deben incluir archivos adjuntos, debemos usar la función SES SendRawEmail en lugar de SendEmail (que se usa en el script Lambda a continuación).

Habiendo agregado la lógica para cargar los archivos con correos electrónicos a S3, podemos pasar a codificar la función Lambda.

Codificación de la secuencia de comandos Lambda

Las funciones Lambda también se denominan funciones sin servidor, no porque no se ejecuten en un servidor, sino porque el desarrollador no necesita preocuparse por el servidor: el desarrollador simplemente proporciona el script y la nube se encarga de aprovisionar el servidor, implementar y ejecutando el guión. Por lo tanto, como se mencionó anteriormente, las funciones de Lambda se cobran en función del tiempo de ejecución de la función.

El siguiente script de Node.js hace el trabajo requerido. Invocada por el evento "Put" de S3, que indica que se ha creado un nuevo objeto en el depósito, la función:

  1. Obtiene la ruta del nuevo objeto (bajo la variable srcKey ) y el depósito (bajo la variable srcBucket ).
  2. Descarga el objeto, a través de s3.getObject .
  3. Analiza el contenido del objeto, a través de JSON.parse(response.Body.toString()) , y extrae los correos electrónicos y la meta del correo electrónico.
  4. Recorre todos los correos electrónicos y los envía a través ses.sendEmail .
 var async = require('async'); var aws = require('aws-sdk'); var s3 = new aws.S3(); exports.handler = function(event, context, callback) { var srcBucket = event.Records[0].s3.bucket.name; var srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")); // Download the file from S3, parse it, and send the emails async.waterfall([ function download(next) { // Download the file from S3 into a buffer. s3.getObject({ Bucket: srcBucket, Key: srcKey }, next); }, function process(response, next) { var file = JSON.parse(response.Body.toString()); var emails = file.emails; var emailsMeta = file.meta; // Check required parameters if (emails === null || emailsMeta === null) { callback('Bad Request: Missing required data: ' + response.Body.toString()); return; } if (emails.length === 0) { callback('Bad Request: No emails provided: ' + response.Body.toString()); return; } var totalEmails = emails.length; var sentEmails = 0; for (var i = 0; i < totalEmails; i++) { var email = emails[i]; var params = { Destination: { ToAddresses: email.to }, Message: { Subject: { Data: email.subject, Charset: emailsMeta.charset } }, Source: emailsMeta.from }; if (emailsMeta.contentType == 'text/html') { params.Message.Body = { Html: { Data: email.message, Charset: emailsMeta.charset } }; } else { params.Message.Body = { Text: { Data: email.message, Charset: emailsMeta.charset } }; } // Send the email var ses = new aws.SES({ "region": "us-east-1" }); ses.sendEmail(params, function(err, data) { if (err) { console.error('Unable to send email due to an error: ' + err); callback(err); } sentEmails++; if (sentEmails == totalEmails) { next(); } }); } } ], function (err) { if (err) { console.error('Unable to send emails due to an error: ' + err); callback(err); } // Success callback(null); }); };

A continuación, debemos cargar y configurar la función Lambda a AWS, lo que implica:

  1. Crear un rol de ejecución que otorgue permisos de Lambda para acceder a S3.
  2. Crear un paquete .zip que contenga todo el código, es decir, la función Lambda que estamos creando + todos los módulos necesarios de Node.js.
  3. Subiendo este paquete a AWS usando una herramienta CLI.

Cómo hacer estas cosas se explica correctamente en el sitio de AWS, en el Tutorial sobre el uso de AWS Lambda con Amazon S3.

Conexión de S3 con la función Lambda

Finalmente, una vez que se crearon el depósito y la función Lambda, debemos vincularlos a ambos, de modo que cada vez que se cree un nuevo objeto en el depósito, se activará un evento para ejecutar la función Lambda. Para ello, vamos al panel de control de S3 y hacemos clic en la fila del depósito, que mostrará sus propiedades:

Visualización de las propiedades del depósito dentro del tablero de S3
Al hacer clic en la fila del depósito, se muestran las propiedades del depósito. (Vista previa grande)

Luego, haciendo clic en Propiedades, nos desplazamos hacia abajo hasta el elemento "Eventos", y allí hacemos clic en Agregar una notificación, e ingresamos los siguientes campos:

  • Nombre: nombre de la notificación, por ejemplo: “EmailSender”;
  • Eventos: “Put”, que es el evento que se activa cuando se crea un nuevo objeto en el depósito;
  • Enviar a: “Función Lambda”;
  • Lambda: nombre de nuestro Lambda recién creado, por ejemplo: “LambdaEmailSender”.
Configuración de S3 con Lambda
Agregar una notificación en S3 para activar un evento para Lambda. (Vista previa grande)

Finalmente, también podemos configurar el depósito S3 para eliminar automáticamente los archivos que contienen los datos del correo electrónico después de un tiempo. Para ello, vamos a la pestaña de Gestión del depósito, y creamos una nueva regla de Ciclo de vida, definiendo a los cuantos días deben caducar los correos:

regla de ciclo de vida
Configuración de una regla de ciclo de vida para eliminar automáticamente archivos del depósito. (Vista previa grande)

Eso es todo. A partir de este momento, al agregar un nuevo objeto en el depósito S3 con el contenido y la meta de los correos electrónicos, activará la función Lambda, que leerá el archivo y se conectará a SES para enviar los correos electrónicos.

Implementé esta solución en mi sitio y volvió a ser rápido: al descargar el envío de correos electrónicos a un proceso externo, si las aplicaciones envían 20 o 5000 correos electrónicos no hace la diferencia, la respuesta al usuario que activó la acción será inmediato.

Conclusión

En este artículo, analizamos por qué enviar muchos correos electrónicos transaccionales en una sola solicitud puede convertirse en un cuello de botella en la aplicación y creamos una solución para solucionar el problema: en lugar de conectarnos al servidor SMTP desde la aplicación (sincrónicamente), podemos envíe los correos electrónicos desde una función externa, de forma asíncrona, basada en una pila de AWS S3 + Lambda + SES.

Al enviar correos electrónicos de forma asíncrona, la aplicación puede administrar el envío de miles de correos electrónicos, pero la respuesta al usuario que activó la acción no se verá afectada. Sin embargo, para asegurarnos de que el usuario no esté esperando a que el correo electrónico llegue a la bandeja de entrada, también decidimos dividir los correos electrónicos en dos grupos, prioritarios y no prioritarios, y enviar solo los correos electrónicos no prioritarios de forma asíncrona. Proporcionamos una implementación para WordPress, que es bastante complicada debido a las limitaciones de la función wp_mail para enviar correos electrónicos.

Una lección de este artículo es que las funcionalidades sin servidor en una aplicación basada en servidor funcionan bastante bien: los sitios que se ejecutan en un CMS como WordPress pueden mejorar su rendimiento al implementar solo funciones específicas en la nube y evitar una gran complejidad que proviene de la migración. sitios altamente dinámicos a una arquitectura completamente sin servidor.