Enviar milhões de e-mails por dia exige uma arquitetura cuidadosa. O gerenciamento de filas, a limitação de taxa e o design da infraestrutura impactam a entregabilidade e a confiabilidade. Veja como construir para escala.
Visão geral da arquitetura
Componentes principais
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ API/App │────▶│ Queue │────▶│ Workers │
└─────────────┘ └─────────────┘ └─────────────┘
│
┌─────────────┐ │
│ Provider │◀───────────┘
│ API │
└─────────────┘
Design da fila
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
};
Limitação de taxa
Limites do provedor
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;
}
}
Throttling dos ISPs
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;
}
Escalonamento de workers
Escalonamento horizontal
// 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)))
);
}
}
Gatilhos de autoescalonamento
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
}
};
Processamento em lote
Loteamento eficiente
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 };
}
Monitoramento em escala
Métricas principais
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')
};
Limiares de alerta
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'
}
];
Considerações de banco de dados
Armazenamento eficiente
// 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'
`);
}
Boas práticas
- —Priorize as filas - E-mails críticos primeiro
- —Respeite os limites de taxa - Tanto do provedor quanto do ISP
- —Monitore tudo - Profundidade da fila, latência, erros
- —Planeje para falhas - Retries, dead letter queues
- —Escalone horizontalmente - Adicione workers, não servidores maiores
- —Faça lotes com eficiência - Agrupe por template, domínio
- —Arquive agressivamente - Não deixe os logs crescerem para sempre
E-mail de alto volume é sobre entrega consistente e confiável em escala. Construa para o volume que você espera, mas projete para um crescimento de 10x.