emailr_
Todos os artigos
usecase·9 min

Failover de email: construindo redundância

reliabilityfailoverarchitecture

Email é infraestrutura crítica. Quando seu provedor primário cai, você precisa de failover. Veja como construir sistemas de email redundantes.

Por que o failover é importante

  • Interrupções de provedores acontecem
  • Limites de taxa da API podem bloquear o envio
  • Problemas de DNS afetam a entregabilidade
  • Falhas regionais impactam a disponibilidade

Arquitetura com múltiplos provedores

Abstração de provedores

interface EmailProvider {
  name: string;
  send(email: Email): Promise<SendResult>;
  checkHealth(): Promise<boolean>;
  getRateLimit(): Promise<RateLimitStatus>;
}

class EmailrProvider implements EmailProvider {
  name = 'emailr';
  
  async send(email: Email): Promise<SendResult> {
    return this.client.emails.send(email);
  }
  
  async checkHealth(): Promise<boolean> {
    try {
      await this.client.health.check();
      return true;
    } catch {
      return false;
    }
  }
}


class SendGridProvider implements EmailProvider {
  name = 'sendgrid';
  // ... implementation
}

Lógica de failover

class EmailService {
  private providers: EmailProvider[];
  private primaryIndex = 0;
  
  constructor(providers: EmailProvider[]) {
    this.providers = providers;
  }
  
  async send(email: Email): Promise<SendResult> {
    const provider = this.providers[this.primaryIndex];
    
    try {
      if (!await provider.checkHealth()) {
        return this.failover(email);
      }
      
      return await provider.send(email);
    } catch (error) {
      return this.failover(email, error);
    }
  }
  
  private async failover(email: Email, error?: Error): Promise<SendResult> {
    for (let i = 0; i < this.providers.length; i++) {
      if (i === this.primaryIndex) continue;
      
      const provider = this.providers[i];
      
      try {
        if (await provider.checkHealth()) {
          const result = await provider.send(email);
          
          // Log failover event
          await logFailover(this.providers[this.primaryIndex].name, provider.name, error);
          
          return result;
        }
      } catch (e) {
        continue;
      }
    }
    
    throw new Error('All email providers failed');
  }
}

Verificação de saúde

Monitoramento proativo de saúde

class HealthChecker {
  private healthStatus: Map<string, boolean> = new Map();
  
  async startMonitoring(providers: EmailProvider[], interval = 30000) {
    setInterval(async () => {
      for (const provider of providers) {
        const healthy = await provider.checkHealth();
        this.healthStatus.set(provider.name, healthy);
        
        if (!healthy) {
          await alertOps(`Provider ${provider.name} unhealthy`);
        }
      }
    }, interval);
  }
  
  isHealthy(providerName: string): boolean {
    return this.healthStatus.get(providerName) ?? true;
  }
}

Padrão Circuit Breaker

class CircuitBreaker {
  private failures = 0;
  private lastFailure?: Date;
  private state: 'closed' | 'open' | 'half-open' = 'closed';
  
  private readonly threshold = 5;
  private readonly timeout = 60000; // 1 minute
  
  async execute<T>(fn: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      if (Date.now() - this.lastFailure!.getTime() > this.timeout) {
        this.state = 'half-open';
      } else {
        throw new Error('Circuit breaker open');
      }
    }
    
    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }
  
  private onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }
  
  private onFailure() {
    this.failures++;
    this.lastFailure = new Date();
    
    if (this.failures >= this.threshold) {
      this.state = 'open';
    }
  }
}

Balanceamento de carga

Distribuição ponderada

class LoadBalancer {
  private providers: Array<{ provider: EmailProvider; weight: number }>;
  
  selectProvider(): EmailProvider {
    const totalWeight = this.providers.reduce((sum, p) => sum + p.weight, 0);
    let random = Math.random() * totalWeight;
    
    for (const { provider, weight } of this.providers) {
      random -= weight;
      if (random <= 0) {
        return provider;
      }
    }
    
    return this.providers[0].provider;
  }
}

// Usage: 70% primary, 30% secondary
const balancer = new LoadBalancer([
  { provider: emailr, weight: 70 },
  { provider: sendgrid, weight: 30 }
]);

Failover baseado em DNS

Múltiplos registros MX

; Primary provider
@ MX 10 mx1.emailr.dev.
@ MX 10 mx2.emailr.dev.

; Backup provider
@ MX 20 mx1.backup-provider.com.
@ MX 20 mx2.backup-provider.com.

Degradação graciosa

async function sendWithDegradation(email: Email): Promise<SendResult> {
  try {
    // Try full-featured send
    return await primaryProvider.send(email);
  } catch (error) {
    // Degrade to basic send (no tracking, simpler template)
    const degradedEmail = {
      ...email,
      html: stripTracking(email.html),
      trackOpens: false,
      trackClicks: false
    };
    
    return await backupProvider.send(degradedEmail);
  }
}

Boas práticas

  1. Múltiplos provedores — pelo menos dois para redundância
  2. Verificações de saúde — monitoramento proativo
  3. Circuit Breakers — evite falhas em cascata
  4. Degradação graciosa — funcionalidade parcial é melhor do que nenhuma
  5. Alertas no failover — saiba quando acontece
  6. Teste o failover — exercícios regulares

Failover de email é um seguro. Você espera nunca precisar, mas quando precisa, é inestimável.

e_

Escrito pela equipe emailr

Construindo infraestrutura de email para desenvolvedores

Pronto para começar a enviar?

Obtenha sua chave API e envie seu primeiro email em menos de 5 minutos. Não é necessário cartão de crédito.