Transactional

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[]) {
  // 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

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);
}

Ready to Build?

Sign up for free and start sending emails today.