Azure App Service SNAT port exhaustion on Sendgrid connection

Viewed 133

I am having a nodejs code trigerring sendgrid email to 1000+ users. When the number users is less (100-200)this works fine. But when there are 1000+ users it fails at a point and the rest of the mails are not triggered. When i check the app service it is shown as SNAT port exhaustion.

userList.forEach(async(Element) => {
        console.log(Element.userId);
        let textContentUser="";
        let emailContentUser="";
        textContentUser=textContent;
        var userData=await db.collection('masterUserDetails').find({"userId":Element.userId}).toArray();
       
        if(userData.length>0)
        {
            
            textContentUser=textContentUser.split("{{FirstName}}").join(userData[0]["givenName"]);
            textContentUser=textContentUser.split("{{FullName}}").join(userData[0]["preferredName"]);
        var leadReviewerData=await db.collection('masterUserDetails').find({"userId":userData[0]["counselorEmail"]}).toArray();
            if(leadReviewerData.length==0)
            {
                textContentUser=textContentUser.split("{{LeadReviewerFullName}}").join(userData[0]["counselorName"]);
                textContentUser=textContentUser.split("{{LeadReviewerFirstName}}").join(userData[0]["counselorName"]);
            }
            else
            {
                textContentUser=textContentUser.split("{{LeadReviewerFullName}}").join(leadReviewerData[0]["preferredName"]);
                textContentUser=textContentUser.split("{{LeadReviewerFirstName}}").join(leadReviewerData[0]["givenName"]);
            }
        
        }
            console.log("final Text cintent: ",textContentUser);
          emailContentUser=emailContent;  
        emailContentUser=emailContentUser.replace("***content***",textContentUser);
        //console.log("final email cintent: ",emailContentUser);
    const msg = {
        to: Element.userId, // Change to your recipient
         bcc:"support_test@abc.com",
        from: {
            "email": "support@abc.com",
            "name": "MY APP"
        }, // Change to your verified sender
        subject: emailSubject,
        html: emailContentUser,
        attachments: [
            {
                filename:"image002.gif",
                content: img2,
                content_id:"image002",
                disposition:"inline"
            },
            {
                filename:"image004.gif",
                content: img4,
                content_id:"image004",
                disposition:"inline"
            }],
            reply_to: {
                "email": "support.test@abc.com",
                "name": "My APP Support"
            },
            send_at: sendAt
    }
   
    console.log("sending mail")
    sgMail
        .send(msg)
        .then((response) => {
            console.log("Success--------"+response[0].statusCode)
            //console.log(response[0].headers)
        })
        .catch((error) => {
           // console.log("error");
           console.log("Error--------"+JSON.stringify(error))
        })            
        
    });

This works in local. works when deployed in azure with lesser users also.

1 Answers

As others have noted, you're running out of SNAT ports to make the HTTP request to the SendGrid API.

I would recommend avoiding making an HTTP request for every email you want to send and instead use the built-in APIs to send multiple emails using one HTTP request.

To do so, add a personalizations property to your msg with an array. Add objects to the array to override the msg properties for that specific personalization, properties like subject, from, to, etc. as documented here. You can add a max of 1000 items to the personalizations array. So if you want to send more than 1000 emails, you'll need to create a separate msg for every 1000 emails, but now you only send a single HTTP request for every 1000 emails, so you shouldn't run into SNAT port errors as quickly.

Now, if you're using a single email body template but want to personalize the email, you can use Substitution Tags in your template and add a substitutions property to your personalization objects with an object holding the data you want to substitute.
Note: The total collective size of your substitutions may not exceed 10,000 bytes per personalization object.

The default substitution wrapper is "{{" and "}}" for the node.js library. Here's an example how using one msg, you can send multiple emails using personalizations and substitutions:

const sgMail = require('@sendgrid/mail');
sgMail.setApiKey(process.env.SENDGRID_API_KEY);

const recipients = [
  {
    emailAddress: "recipient1@email.address",
    preferredName: "Jonny",
    counselorName: "Smith"
  },
  {
    emailAddress: "recipient2@email.address",
    preferredName: "Jay",
    counselorName: "Smith"
  }
];

const msg = {
  // this will create an array of objects with `to` and `substitutions ` properties
  personalizations: recipients.map(r => {
    return {
      to: r.emailAddress,
      substitutions: {
        preferredName: r.preferredName,
        counselorName: r.counselorName
      }
    }
  }),
  from: 'your@sendgrid.sender',
  subject: 'Ahoy!',
  html: `<p>Ahoy {{preferredName}}!</p>
  <p>It is a pleasure to meet you.</p>
  <p>Best, {{counselorName}}</p>`
};

sgMail.send(msg).then(() => {
  console.log('emails sent successfully!');
}).catch(error => {
  console.log(error);
});

Pay attention to the html template containing substitution tags like {{preferredName}} and {{counselorName}}. These tag names have to match with the keys of the object in the substitutions property.

Warning: Substitution tags will not automatically HTML encode user input, so you have to HTML encode it yourself to prevent HTML injection attacks. You can learn more about HTML injection attacks here.


If you don't need to store the email body template in your code, you can use Dynamic Email Template as shown in this sample. You can create Dynamic Email Templates in the SendGrid app and use handlebar templating to add template data into your emails.

Updating the previous example with Dynamic Email Templates would look like this:

const msg = {
  // this will create an array of objects with `to` and `dynamicTemplateData` properties
  personalizations: recipients.map(r => {
    return {
      to: r.emailAddress,
      dynamicTemplateData: {
        preferredName: r.preferredName,
        counselorName: r.counselorName
      }
    }
  }),
  from: 'your@sendgrid.sender',
  templateId: 'd-[REPLACE_WITH_YOUR_TEMPLATE_ID]'
};

sgMail.send(msg).then(() => {
  console.log('emails sent successfully!');
}).catch(error => {
  console.log(error);
});

The subject and substitutions properties have been removed. The templateId and dynamicTemplateData have been added.

Note: Dynamic Email Templates use handlebar templating which will HTML encode user input, protecting you from HTML injection.


Last suggestion: Twilio recommends schedule your bulk emails by using the sendAt property instead of sending them right now.

Quoting from the SendGrid docs:

This technique allows for a more efficient way to distribute large email requests and can improve overall mail delivery time performance. This functionality:

  • Improves efficiency of processing and distributing large volumes of email.
  • Reduces email pre-processing time.
  • Enables you to time email arrival to increase open rates.
  • Is available for free to all SendGrid customers.

Related