Facebook Pixel

Amazon SES Error Handling and Retries in Node.js Bounces, Complaints, and Failures

Learn how to handle Amazon SES errors in Node.js bounces, complaints, throttling, authentication failures, and implementing retry logic.

Amazon SES Error Handling and Retries

Emails can fail for many reasons. This guide covers handling SES errors, bounces, complaints, and implementing retry logic.

Common SES Errors

ErrorCodeCauseAction
Invalid recipient400Bad email formatValidate before sending
Sandbox restriction453Unverified recipientVerify email or request production
Throttling429Too many emails/secondSlow down or request quota increase
Authentication failed403Wrong credentialsCheck AWS keys
Message rejected554Spam content or blockedFix content, check reputation
BounceN/ARecipient doesn't existMark as bounced, don't resend
ComplaintN/AUser marked as spamMark as complained, don't resend

Basic Error Handling

const { SendEmailCommand } = require("@aws-sdk/client-ses"); const sesClient = require("./sesClient"); const sendEmailWithRetry = async (params, retries = 3) => { for (let i = 0; i < retries; i++) { try { const result = await sesClient.send(new SendEmailCommand(params)); return result; } catch (err) { console.error(`Email attempt ${i + 1} failed:`, err.message); // Don't retry on these errors if (err.name === "MessageRejected") { throw new Error(`Email rejected: ${err.message}`); } if (err.name === "InvalidParameterValue") { throw new Error(`Invalid email: ${err.message}`); } // Retry on these errors if (err.name === "ThrottlingException" || err.statusCode === 429) { const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s await new Promise(resolve => setTimeout(resolve, delay)); continue; } if (i === retries - 1) throw err; } } };

Handling Bounces

Bounces occur when an email can't be delivered:

Hard bounce Permanent failure (invalid email, domain doesn't exist) Soft bounce Temporary failure (mailbox full, server down)

// Set up SNS notification for bounces // SES → Configuration Sets → Event Publishing → SNS topic // SNS webhook handler router.post("/ses/webhook", express.json(), asyncHandler(async (req, res) => { const notification = JSON.parse(req.body.Message); if (notification.notificationType === "Bounce") { const bounce = notification.bounce; const email = bounce.bouncedRecipients[0].emailAddress; const bounceType = bounce.bounceType; // "Permanent" or "Transient" if (bounceType === "Permanent") { await User.findOneAndUpdate( { email }, { emailBounced: true, emailBouncedAt: new Date() } ); console.log(`Permanent bounce: ${email}`); } } if (notification.notificationType === "Complaint") { const complaint = notification.complaint; const email = complaint.complainedRecipients[0].emailAddress; await User.findOneAndUpdate( { email }, { emailComplained: true, emailComplainedAt: new Date() } ); console.log(`Complaint: ${email}`); } res.status(200).json({ status: "ok" }); }));

Check Before Sending

const canSendEmail = (user) => { if (!user) return false; if (user.emailBounced) return false; if (user.emailComplained) return false; if (!user.email) return false; return true; }; const sendSafeEmail = async (user, template, data) => { if (!canSendEmail(user)) { console.log(`Skipping email to ${user?.email} (bounced or complained)`); return null; } try { return await sendEmailWithRetry({ Source: process.env.SES_FROM, Destination: { ToAddresses: [user.email] }, Message: { Subject: { Data: templates[template](data).subject, Charset: "UTF-8" }, Body: { Html: { Data: templates[template](data).html, Charset: "UTF-8" }, Text: { Data: templates[template](data).text, Charset: "UTF-8" } } } }); } catch (err) { console.error("Email failed after retries:", err); // Store failed email for later retry await FailedEmail.create({ userId: user._id, email: user.email, template, data, error: err.message, createdAt: new Date() }); return null; } };

Rate Limiting

SES has sending limits. Don't exceed them:

// Check sending limits const { GetSendQuotaCommand } = require("@aws-sdk/client-ses"); const checkQuota = async () => { const result = await sesClient.send(new GetSendQuotaCommand({})); console.log({ max24HourSend: result.Max24HourSend, maxSendRate: result.MaxSendRate, // emails per second sentLast24Hours: result.SentLast24Hours }); if (result.SentLast24Hours >= result.Max24HourSend) { throw new Error("Daily sending limit reached"); } };

Logging Email Events

// Log every email send attempt const logEmail = async (userId, email, template, status, error = null) => { await EmailLog.create({ userId, email, template, status, // "sent", "failed", "bounced" error, timestamp: new Date() }); }; // Use in sendSafeEmail await logEmail(user._id, user.email, template, "sent");

The Takeaway

SES error handling involves: retrying on throttling (429) with exponential backoff, not retrying on rejections (bad content or invalid email), handling bounces and complaints via SNS webhooks, checking before sending (skip bounced/complained emails), rate limiting to stay within SES quotas, and logging all email events for debugging. Use a dead-letter queue or database table for permanently failed emails.

Invalid recipient (400, bad email format), sandbox restriction (453, unverified recipient), throttling (429, too many emails/second), authentication failed (403, wrong credentials), message rejected (554, spam content), bounce (recipient doesn't exist), and complaint (user marked as spam).

Set up SNS notifications in SES configuration sets. Create a webhook endpoint that receives SNS notifications. When a bounce occurs, parse the notification, extract the email address and bounce type. For permanent bounces, mark the user as emailBounced in your database and never send to them again.

Implement retry with exponential backoff. Retry on ThrottlingException (429) with delays of 1s, 2s, 4s. Don't retry on MessageRejected or InvalidParameterValue (fix the issue instead). Use Bull queue for automatic retry with attempts and backoff settings.

Track bounces in your database (emailBounced: true, emailBouncedAt). Before sending, check canSendEmail(user) which returns false if user.emailBounced or user.emailComplained is true. Never send to bounced or complained addresses to protect your sender reputation.

Use GetSendQuotaCommand from the AWS SDK. It returns Max24HourSend (daily limit), MaxSendRate (emails per second), and SentLast24Hours (sent today). If SentLast24Hours >= Max24HourSend, stop sending. Request a quota increase from SES console if needed.

Ready to master Node.js completely?

Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.

Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.
Please Login.