emailr_
Tous les articles
usecase·11 min

E-mails à haut volume : mise à l’échelle jusqu’à des millions par jour

scaleinfrastructurearchitecture

Envoyer des millions d’e-mails par jour nécessite une architecture soignée. La gestion des files d’attente, le rate limiting et la conception de l’infrastructure influent sur la délivrabilité et la fiabilité. Voici comment construire pour passer à l’échelle.

Aperçu de l’architecture

Composants principaux

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   API/App   │────▶│    Queue    │────▶│   Workers   │
└─────────────┘     └─────────────┘     └─────────────┘
                                               │
                    ┌─────────────┐            │
                    │   Provider  │◀───────────┘
                    │     API     │
                    └─────────────┘

Conception des files d’attente

interface EmailJob {
  id: string;
  priority: 'critical' | 'high' | 'normal' | 'low';
  payload: {
    to: string;
    from: string;
    subject: string;
    html: string;
  };
  metadata: {
    userId: string;
    campaignId?: string;
    scheduledFor?: Date;
  };
  attempts: number;
  maxAttempts: number;
  createdAt: Date;
}


// Priority queues
const queues = {
  critical: 'email:critical',  // Password resets, 2FA
  high: 'email:high',          // Transactional
  normal: 'email:normal',      // Notifications
  low: 'email:low'             // Marketing, digests
};

Rate limiting

Limites côté fournisseur

class RateLimiter {
  private limits: Map<string, { count: number; resetAt: Date }> = new Map();
  
  async canSend(provider: string): Promise<boolean> {
    const limit = this.limits.get(provider);
    
    if (!limit || limit.resetAt < new Date()) {
      this.limits.set(provider, {
        count: 1,
        resetAt: addSeconds(new Date(), 1)
      });
      return true;
    }
    
    if (limit.count >= PROVIDER_RATE_LIMITS[provider]) {
      return false;
    }
    
    limit.count++;
    return true;
  }
}

Bridage des FAI

const ispLimits = {
  'gmail.com': { perHour: 500, perDay: 5000 },
  'yahoo.com': { perHour: 200, perDay: 2000 },
  'outlook.com': { perHour: 300, perDay: 3000 }
};

async function getThrottledBatch(emails: Email[]): Promise<Email[]> {
  const byDomain = groupBy(emails, e => getDomain(e.to));
  const batch: Email[] = [];
  
  for (const [domain, domainEmails] of Object.entries(byDomain)) {
    const limit = ispLimits[domain] || { perHour: 1000 };
    const sent = await getSentCount(domain, 'hour');
    const available = limit.perHour - sent;
    
    batch.push(...domainEmails.slice(0, available));
  }
  
  return batch;
}

Mise à l’échelle des workers

Mise à l’échelle horizontale

// Worker configuration
const workerConfig = {
  concurrency: 50,           // Emails per worker
  batchSize: 100,            // Fetch from queue
  pollInterval: 100,         // ms between polls
  maxRetries: 3,
  retryDelay: [1000, 5000, 30000] // Exponential backoff
};

async function processQueue(queue: string) {
  while (true) {
    const jobs = await redis.lpop(queue, workerConfig.batchSize);
    
    if (jobs.length === 0) {
      await sleep(workerConfig.pollInterval);
      continue;
    }
    
    await Promise.all(
      jobs.map(job => processJob(JSON.parse(job)))
    );
  }
}

Déclencheurs d’auto-scaling

const scalingRules = {
  scaleUp: {
    queueDepth: 10000,      // Jobs waiting
    processingTime: 5000,   // ms per job
    errorRate: 0.05         // 5% errors
  },
  scaleDown: {
    queueDepth: 100,
    idleTime: 300000        // 5 minutes idle
  }
};

Traitement par lots

Regroupement efficace

async function sendBatch(emails: Email[]): Promise<BatchResult> {
  // Group by template for efficiency
  const byTemplate = groupBy(emails, 'templateId');
  const results: SendResult[] = [];
  
  for (const [templateId, templateEmails] of Object.entries(byTemplate)) {
    // Compile template once
    const template = await getCompiledTemplate(templateId);
    
    // Send in parallel with concurrency limit
    const batchResults = await pMap(
      templateEmails,
      email => sendWithTemplate(email, template),
      { concurrency: 50 }
    );
    
    results.push(...batchResults);
  }
  
  return { sent: results.filter(r => r.success).length, results };
}

Supervision à l’échelle

Métriques clés

const metrics = {
  // Throughput
  emailsPerSecond: gauge('emails_per_second'),
  emailsPerMinute: gauge('emails_per_minute'),
  
  // Latency
  queueLatency: histogram('queue_latency_ms'),
  sendLatency: histogram('send_latency_ms'),
  
  // Errors
  errorRate: gauge('error_rate'),
  bounceRate: gauge('bounce_rate'),
  
  // Queue health
  queueDepth: gauge('queue_depth'),
  oldestJob: gauge('oldest_job_age_seconds')
};

Seuils d’alerte

const alerts = [
  {
    name: 'high_queue_depth',
    condition: 'queue_depth > 100000',
    severity: 'warning'
  },
  {
    name: 'high_error_rate',
    condition: 'error_rate > 0.05',
    severity: 'critical'
  },
  {
    name: 'slow_processing',
    condition: 'queue_latency_p99 > 60000',
    severity: 'warning'
  }
];

Considérations relatives à la base de données

Stockage efficace

// Partition by date for easy cleanup
const emailLogSchema = {
  tableName: 'email_logs',
  partitionBy: 'RANGE (created_at)',
  indexes: [
    'user_id',
    'campaign_id',
    'status',
    'created_at'
  ],
  retention: '90 days'
};

// Archive old data
async function archiveOldEmails() {
  await db.query(`
    INSERT INTO email_logs_archive
    SELECT * FROM email_logs
    WHERE created_at < NOW() - INTERVAL '90 days'
  `);
  
  await db.query(`
    DELETE FROM email_logs
    WHERE created_at < NOW() - INTERVAL '90 days'
  `);
}

Bonnes pratiques

  1. Priorisez les files - E-mails critiques en premier
  2. Respectez les rate limits - Côté provider et côté FAI
  3. Surveillez tout - Profondeur de file, latence, erreurs
  4. Planifiez les pannes - Retries, dead letter queues
  5. Mettez à l’échelle horizontalement - Ajoutez des workers, pas des serveurs plus gros
  6. Regroupez efficacement - Par modèle, par domaine
  7. Archivez de manière agressive - Ne laissez pas les logs croître indéfiniment

L’e-mail à haut volume consiste à assurer une délivrabilité régulière et fiable à l’échelle. Construisez pour le volume que vous attendez, mais concevez pour une croissance 10x.

e_

Écrit par l'équipe emailr

Nous construisons l'infrastructure email pour les développeurs

Prêt à commencer à envoyer ?

Obtenez votre clé API et envoyez votre premier email en moins de 5 minutes. Aucune carte de crédit requise.