Send Batch Emails
Learn how to send batch emails efficiently with the Transactional TypeScript SDK. Includes error handling and rate limiting strategies.
Intermediate
email
batch
performance
bulk
Prerequisites
- Node.js 18+ installed
- Transactional account with API key
- Verified sending domain
- Basic understanding of async/await
CODE
Full Example
import { Transactional } from '@transactional/sdk';
const client = new Transactional({
apiKey: process.env.TRANSACTIONAL_API_KEY!,
});
interface Recipient {
email: string;
name: string;
data: Record<string, unknown>;
}
async function sendBatchEmails(recipients: Recipient[]) {
// Build the batch request
const emails = recipients.map((recipient) => ({
from: 'hello@yourdomain.com',
to: recipient.email,
subject: `Hello, ${recipient.name}!`,
html: `
<h1>Welcome, ${recipient.name}!</h1>
<p>Thank you for joining our platform.</p>
`,
text: `Welcome, ${recipient.name}! Thank you for joining our platform.`,
metadata: {
userId: recipient.data.userId,
campaign: 'welcome-batch',
},
}));
// Send batch (up to 100 emails per request)
const { data, error } = await client.emails.sendBatch({
emails,
});
if (error) {
console.error('Batch send failed:', error.message);
return { success: false, error };
}
// Log results
console.log(`Batch sent: ${data.successful} succeeded, ${data.failed} failed`);
// Handle individual failures
for (const failure of data.failures) {
console.error(`Failed to send to ${failure.email}: ${failure.reason}`);
}
return { success: true, data };
}
// Example usage
const recipients: Recipient[] = [
{ email: 'user1@example.com', name: 'Alice', data: { userId: '1' } },
{ email: 'user2@example.com', name: 'Bob', data: { userId: '2' } },
{ email: 'user3@example.com', name: 'Charlie', data: { userId: '3' } },
];
sendBatchEmails(recipients);
Understanding Batch Sending
Batch sending allows you to send multiple emails in a single API request, which is more efficient than individual sends for bulk operations.
When to Use Batch Sending
- Sending notifications to multiple users about the same event
- Welcome email campaigns
- Shipping notifications for multiple orders
- Weekly digest emails
- Any scenario with 10+ emails to send
Limits and Considerations
| Limit | Value |
|---|---|
| Max emails per batch | 100 |
| Max request size | 10 MB |
| Rate limit | 1,000 emails/minute |
Batch Response Handling
The batch send response provides detailed results:
interface BatchSendResult {
successful: number; // Count of successfully queued emails
failed: number; // Count of failed emails
results: {
email: string;
id?: string; // Message ID if successful
status: 'queued' | 'failed';
error?: string; // Error message if failed
}[];
failures: {
email: string;
reason: string;
}[];
}Handling Partial Failures
Always check for partial failures in batch sends:
const { data, error } = await client.emails.sendBatch({ emails });
if (error) {
// Complete failure - no emails were processed
console.error('Batch request failed:', error.message);
return;
}
// Check for partial failures
if (data.failed > 0) {
console.log(`${data.failed} emails failed out of ${data.successful + data.failed}`);
for (const failure of data.failures) {
// Log for retry or investigation
await logFailedEmail(failure.email, failure.reason);
}
}Retry Strategy for Failures
Implement exponential backoff for transient failures:
async function sendWithRetry(
emails: EmailPayload[],
maxRetries = 3
): Promise<BatchSendResult> {
let lastError: Error | null = null;
for (let attempt = 0; attempt < maxRetries; attempt++) {
const { data, error } = await client.emails.sendBatch({ emails });
if (!error) {
return data;
}
// Check if error is retryable
if (error.code === 'RATE_LIMITED' || error.code === 'SERVICE_UNAVAILABLE') {
const delay = Math.pow(2, attempt) * 1000; // 1s, 2s, 4s
console.log(`Attempt ${attempt + 1} failed, retrying in ${delay}ms...`);
await new Promise((resolve) => setTimeout(resolve, delay));
lastError = new Error(error.message);
} else {
// Non-retryable error
throw new Error(error.message);
}
}
throw lastError || new Error('Max retries exceeded');
}Performance Tips
1. Use Templates for Repeated Content
When sending similar emails to many recipients, use server-side templates to reduce payload size:
// Instead of sending full HTML for each email...
const emails = recipients.map(r => ({
to: r.email,
templateId: 'welcome-email', // Much smaller payload
templateData: { name: r.name },
}));2. Process in Parallel with Limits
For very large sends, process batches in parallel but limit concurrency:
import pLimit from 'p-limit';
const limit = pLimit(5); // Max 5 concurrent batch requests
const chunks = chunkArray(allRecipients, 100);
const results = await Promise.all(
chunks.map(chunk =>
limit(() => client.emails.sendBatch({ emails: buildEmails(chunk) }))
)
);3. Stream Large Datasets
For millions of emails, stream from your database rather than loading all into memory:
async function* getRecipientsStream(cursor?: string) {
while (true) {
const batch = await db.users.findMany({
take: 1000,
cursor: cursor ? { id: cursor } : undefined,
});
if (batch.length === 0) break;
yield batch;
cursor = batch[batch.length - 1].id;
}
}
for await (const recipientBatch of getRecipientsStream()) {
await sendBatchEmails(recipientBatch);
}