Send Batch Emails

Learn how to send batch emails efficiently with the Transactional TypeScript SDK. Includes error handling and rate limiting strategies.

Intermediate
15 minutes
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[]) {
  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',
    },
  }));
  const { data, error } = await client.emails.sendBatch({
    emails,
  });
  if (error) {
    console.error('Batch send failed:', error.message);
    return { success: false, error };
  }
  console.log(`Batch sent: ${data.successful} succeeded, ${data.failed} failed`);
  for (const failure of data.failures) {
    console.error(`Failed to send to ${failure.email}: ${failure.reason}`);
  }
  return { success: true, data };
}

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

LimitValue
Max emails per batch100
Max request size10 MB
Rate limit1,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);
}

Sources & References

  1. [1]Node.js Streams DocumentationNode.js Foundation
  2. [2]MDN Web Docs - Promise.allSettled()Mozilla
  3. [3]RFC 5321 - Simple Mail Transfer ProtocolInternet Engineering Task Force

Ready to Build?

Sign up for free and start sending emails today.