emailr_
All articles
usecase·9 min

Serverless email: Patterns for Lambda and Edge

serverlesslambdaedge

Serverless functions have unique constraints for email: cold starts, timeouts, and statelessness. Here's how to send email effectively from Lambda, Edge functions, and similar environments.

Serverless constraints

  • Cold start latency
  • Execution time limits (often 10-30 seconds)
  • No persistent connections
  • Stateless execution
  • Pay-per-invocation pricing

Direct sending pattern

Simple Lambda email

import { Emailr } from 'emailr';

const emailr = new Emailr(process.env.EMAILR_API_KEY);

export async function handler(event: APIGatewayEvent) {
  const { to, subject, html } = JSON.parse(event.body);
  
  try {
    const result = await emailr.emails.send({
      from: '[email protected]',
      to,
      subject,
      html
    });
    
    return {
      statusCode: 200,
      body: JSON.stringify({ id: result.id })
    };
  } catch (error) {
    return {
      statusCode: 500,
      body: JSON.stringify({ error: 'Failed to send email' })
    };
  }
}

Handling cold starts

// Initialize outside handler for connection reuse
const emailr = new Emailr(process.env.EMAILR_API_KEY);

// Warm connections with provisioned concurrency
export const handler = async (event) => {
  // Handler code - connection already warm
};

Queue-based pattern

For high volume or non-critical emails, queue instead of sending directly:

import { SQSClient, SendMessageCommand } from '@aws-sdk/client-sqs';

const sqs = new SQSClient({});

export async function queueEmail(event: APIGatewayEvent) {
  const email = JSON.parse(event.body);
  
  await sqs.send(new SendMessageCommand({
    QueueUrl: process.env.EMAIL_QUEUE_URL,
    MessageBody: JSON.stringify(email),
    MessageAttributes: {
      priority: {
        DataType: 'String',
        StringValue: email.priority || 'normal'
      }
    }
  }));
  
  return { statusCode: 202, body: JSON.stringify({ queued: true }) };
}

// Separate worker Lambda processes queue
export async function processEmailQueue(event: SQSEvent) {
  const results = await Promise.allSettled(
    event.Records.map(record => {
      const email = JSON.parse(record.body);
      return emailr.emails.send(email);
    })
  );
  
  // Failed messages return to queue automatically
  const failures = results.filter(r => r.status === 'rejected');
  if (failures.length > 0) {
    throw new Error(`${failures.length} emails failed`);
  }
}

Edge function patterns

Cloudflare Workers

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const { to, subject, html } = await request.json();
    
    const response = await fetch('https://api.emailr.dev/emails', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${env.EMAILR_API_KEY}`,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ from: '[email protected]', to, subject, html })
    });
    
    return new Response(JSON.stringify({ sent: response.ok }), {
      headers: { 'Content-Type': 'application/json' }
    });
  }
};

Vercel Edge Functions

import { NextResponse } from 'next/server';

export const runtime = 'edge';

export async function POST(request: Request) {
  const { to, subject, html } = await request.json();
  
  const response = await fetch('https://api.emailr.dev/emails', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.EMAILR_API_KEY}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ from: '[email protected]', to, subject, html })
  });
  
  return NextResponse.json({ sent: response.ok });
}

Timeout handling

// Set aggressive timeouts for serverless
const emailr = new Emailr(process.env.EMAILR_API_KEY, {
  timeout: 5000 // 5 seconds max
});

export async function handler(event) {
  try {
    const result = await Promise.race([
      emailr.emails.send(email),
      new Promise((_, reject) => 
        setTimeout(() => reject(new Error('Timeout')), 8000)
      )
    ]);
    
    return { statusCode: 200, body: JSON.stringify(result) };
  } catch (error) {
    if (error.message === 'Timeout') {
      // Queue for retry instead of failing
      await queueForRetry(email);
      return { statusCode: 202, body: JSON.stringify({ queued: true }) };
    }
    throw error;
  }
}

Batch processing

// Process multiple emails efficiently
export async function batchHandler(event: SQSEvent) {
  const emails = event.Records.map(r => JSON.parse(r.body));
  
  // Batch API call if supported
  const result = await emailr.emails.sendBatch(emails);
  
  return {
    batchItemFailures: result.failed.map(f => ({
      itemIdentifier: f.messageId
    }))
  };
}

Best practices

  1. Initialize outside handler - Reuse connections
  2. Use queues for volume - Don't block on sends
  3. Set timeouts - Don't exceed function limits
  4. Handle failures gracefully - Queue for retry
  5. Monitor cold starts - Use provisioned concurrency if needed
  6. Keep payloads small - Minimize data transfer

Serverless email is about working within constraints. Design for the execution model and you'll have reliable, scalable email.

e_

Written by the emailr team

Building email infrastructure for developers

Ready to start sending?

Get your API key and send your first email in under 5 minutes. No credit card required.