emailr_
Alle Artikel
usecase·9 min

E-Mail-Failover: Redundanz aufbauen

reliabilityfailoverarchitecture

E-Mail ist kritische Infrastruktur. Wenn dein primärer Provider ausfällt, brauchst du Failover. So baust du redundante E-Mail-Systeme.

Warum Failover wichtig ist

  • Provider-Ausfälle passieren
  • API-Rate-Limits können das Senden blockieren
  • DNS-Probleme beeinträchtigen die Zustellbarkeit
  • Regionale Ausfälle beeinträchtigen die Verfügbarkeit

Multi-Provider-Architektur

Provider-Abstraktion

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
}

Failover-Logik

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

Health-Checks

Proaktives Health-Monitoring

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

Circuit-Breaker-Pattern

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

Load Balancing

Gewichtete Verteilung

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

DNS-basiertes Failover

Mehrere MX-Records

; 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.

Graceful Degradation

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

Best Practices

  1. Mehrere Provider - Mindestens zwei für Redundanz
  2. Health-Checks - Proaktives Monitoring
  3. Circuit Breakers - Kaskadierende Ausfälle verhindern
  4. Graceful Degradation - Teilfunktionalität ist besser als gar keine
  5. Bei Failover alarmieren - Wissen, wann es passiert
  6. Failover testen - Regelmäßige Übungen

E-Mail-Failover ist eine Versicherung. Du hoffst, dass du sie nie brauchst, aber wenn doch, ist sie unschätzbar wertvoll.

e_

Geschrieben vom emailr-Team

Wir bauen Email-Infrastruktur für Entwickler

Bereit zum Senden?

Hol dir deinen API-Schlüssel und sende deine erste E-Mail in unter 5 Minuten. Keine Kreditkarte erforderlich.