Skip to content

chore: parallelize follow-up email sending with Promise.all #1239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from

Conversation

0xajinkya
Copy link

@0xajinkya 0xajinkya commented Apr 27, 2025

Type of change

  • 📖 Refactoring / dependency upgrade / documentation
  • 🔨 Breaking change
  • 🐛 Bug fix
  • 🌟 New feature

Description

Previously, follow-up emails were sent sequentially inside a for...of loop, leading to slower execution. This change uses Promise.all to send emails in parallel, significantly improving the speed of the process.

This PR improves the performance of the sendFollowUpEmails function by replacing the sequential email sending (using a for...of loop) with parallel sending using Promise.all.
This change speeds up the process, especially when there are many users to email, without affecting the functionality.

A picture tells a thousand words (if any)

Before this PR

Screenshot from 2025-04-28 03-40-11

After this PR

Screenshot from 2025-04-28 03-40-40

Related Issue (optional)

Previously, follow-up emails were sent sequentially inside a for...of loop, leading to slower execution. This change uses Promise.all to send emails in parallel, significantly improving the speed of the process.
Copy link

vercel bot commented Apr 27, 2025

@0xajinkya is attempting to deploy a commit to the OpenStatus Team on Vercel.

A member of the Team first needs to authorize it.

@thibaultleouay
Copy link
Member

Thanks for you PR

My concern if we parallelize every send email we will hit resend rate limit
what we could do but it might require a bit more work is to use sendBatch
https://resend.com/docs/api-reference/emails/send-batch-emails

@0xajinkya
Copy link
Author

Thanks for you PR

My concern if we parallelize every send email we will hit resend rate limit what we could do but it might require a bit more work is to use sendBatch https://resend.com/docs/api-reference/emails/send-batch-emails

Hey @thibaultleouay, I am looking into this. Will keep you updated.

Improve the follow-up email system by:
- Optimizing database queries to select only necessary email fields
- Implementing batched email sending (80 emails per batch)
- Adding proper rate limit detection and handling
- Improving error logging and recovery
@0xajinkya
Copy link
Author

Hey @thibaultleouay,

Can you look here?

Following are the key changes:

  1. Database optimization: Now only selecting the email field from users instead of fetching complete user records, reducing memory usage and database load.

  2. Batch processing: Implemented batching of emails (80 per batch) to prevent overwhelming the email service and improve throughput.

  3. Rate limit handling: Added specific detection of rate_limit_exceeded errors, which stops further sending attempts when limits are hit to prevent wasted API calls.

  4. Error handling improvements: Enhanced error catching with more specific error types and better logging of failures.

  5. Process visibility: Added additional logging to track batch progress.

These changes should make the follow-up email process more reliable and prevent issues when sending to large numbers of users. The batching approach also provides better visibility into the sending process and enables easier recovery when errors occur.

// Chunk emails into batches of 100
const batchSize = 80;
for (let i = 0; i < validEmails.length; i += batchSize) {
const batch = validEmails.slice(i, i + batchSize) as string[];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not a huge fan of as string[], cant we auto infer it instead?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Latest change takes care of this. Please have a look.

throw result.error;
}

console.error(`Batch send error:`, result.error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also log the error above move this after line 59

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thing is, if the batch send fails specifically due to rate limit, it is intuitional that subsequent batches will fail as well. So, that's why I am not logging that particular error. And, instead handling it in the loop itself (Logging it and breaking from the loop). Any other error, are being simply consolled.

}

console.log(`Sent follow up email to ${req.to}`);
} catch (err: any) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure about the try/catch

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is explained in the latest reply to above conversation. Have a look.

The changes improve email delivery reliability by:

- Enhancing email validation with stronger type guards
- Adding proper break statement for rate limit errors to prevent wasteful API calls
- Simplifying error flow control with early returns and consistent error handling
- Restructuring try/catch blocks for better exception management
- Improving TypeScript type safety with more specific error handling

These modifications ensure the system gracefully handles rate limiting while providing clearer logs for debugging. Email validation now properly checks for empty strings, preventing attempts to send to invalid addresses.
@0xajinkya
Copy link
Author

0xajinkya commented Apr 30, 2025

Any updates @thibaultleouay?

@thibaultleouay
Copy link
Member

@0xajinkya Just pushed some update in your pr let me know if you have any question

Initially, we were not checking type of email and calling trim() on it, this improvision check whether email is really a string and then calls trim() on it.
@0xajinkya
Copy link
Author

Hey @thibaultleouay, All good, added a minor fix. Also, I am wondering why are we doing this:

const validEmails = users .map((u) => u.email) .filter((email) => email !== null) // I don't know why but I can't have both filter at the same time .filter((email) => typeof email === "string" && email.trim() !== "");

This seems repetitive.

@thibaultleouay
Copy link
Member

@0xajinkya We are filtering null first, then removing empty email

.map((u) => u.email)
.filter((email) => email !== null)
// I don't know why but I can't have both filter at the same time
.filter((email) => typeof email === "string" && email.trim() !== "");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CleanShot 2025-05-01 at 12 01 49@2x

I dont understand the typeof email here, because the type is already infered ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it was totally not needed. It was error from my end. Changing it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants