Best practices for high-volume email sending including batching, rate management, and recipient grouping
When your application needs to send thousands or millions of emails, how you structure and pace those sends directly affects deliverability, performance, and cost. This guide covers practical strategies for batching, rate management, and recipient grouping at scale.
Using per-recipient substitution data with templates is more efficient than sending individual API calls for each recipient. One request with 100 recipients is faster than 100 individual requests.
For large sends, use a job queue to manage the workload. This gives you control over pacing, retry logic, and failure handling.
Copy
// Producer: Split recipients into batches and enqueueasync function enqueueBulkSend(recipients, templateId) { const BATCH_SIZE = 200; for (let i = 0; i < recipients.length; i += BATCH_SIZE) { const batch = recipients.slice(i, i + BATCH_SIZE); await queue.add('send-email-batch', { recipients: batch, templateId, batchNumber: Math.floor(i / BATCH_SIZE) + 1, totalBatches: Math.ceil(recipients.length / BATCH_SIZE) }); }}// Consumer: Process each batch with rate limitingqueue.process('send-email-batch', async (job) => { const { recipients, templateId } = job.data; const response = await fetch('https://app.lettr.com/api/emails', { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.LETTR_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ from: 'newsletter@mail.example.com', to: recipients, templateId }) }); if (response.status === 429) { const retryAfter = response.headers.get('Retry-After'); throw new Error(`Rate limited. Retry after ${retryAfter}s`); } if (!response.ok) { throw new Error(`Send failed: ${response.status}`); }});
Configure your queue workers with appropriate concurrency limits. Running too many workers in parallel will hit Lettr’s rate limits. Start with 2–3 concurrent workers and adjust based on observed throughput.
Lettr enforces rate limits to protect deliverability for all senders. Understanding and working within these limits is essential for high-volume sending.
Send to your most engaged recipients first. This front-loads positive signals (opens, clicks) that improve your reputation for the remainder of the send.
Send Order
Segment
Why
First
Opened/clicked in last 7 days
Highest engagement probability — builds positive signals
Second
Opened/clicked in last 30 days
Still engaged, reinforces positive reputation
Third
Opened/clicked in last 90 days
Moderate risk, but still opted-in
Last (or skip)
No engagement in 90+ days
Highest risk — consider a re-engagement campaign first
When sending to large lists, consider grouping recipients by their email domain. This lets you monitor deliverability per provider and respond if a specific provider starts throttling.
Copy
function groupByDomain(recipients) { const groups = {}; for (const recipient of recipients) { const domain = recipient.email.split('@')[1]; if (!groups[domain]) groups[domain] = []; groups[domain].push(recipient); } return groups;}// Send to each domain group with independent monitoringconst groups = groupByDomain(allRecipients);for (const [domain, recipients] of Object.entries(groups)) { console.log(`Sending ${recipients.length} emails to ${domain}`); await enqueueBulkSend(recipients, templateId);}
Set up a simple counter to track bulk send progress:
Copy
const bulkSendMetrics = { sent: 0, delivered: 0, bounced: 0, complained: 0, deferred: 0};app.post('/webhooks/lettr', (req, res) => { const event = req.body; switch (event.type) { case 'email.delivered': bulkSendMetrics.delivered++; break; case 'email.bounced': bulkSendMetrics.bounced++; break; case 'email.complained': bulkSendMetrics.complained++; break; case 'email.deferred': bulkSendMetrics.deferred++; break; } // Alert if complaint rate exceeds threshold const total = bulkSendMetrics.delivered + bulkSendMetrics.bounced; if (total > 100 && bulkSendMetrics.complained / total > 0.003) { alertTeam('Complaint rate exceeding 0.3% — consider pausing send'); } res.sendStatus(200);});
Always set up monitoring before starting a bulk send. Discovering a problem after sending to your entire list is much worse than catching it after the first few thousand and pausing.
Blasting your full list as fast as possible overwhelms receiving servers and triggers rate limiting or blocks. Pace your sends and start with engaged recipients.
Not filtering suppressed recipients before sending
Always check your suppression list before a bulk send. Sending to previously bounced or complained addresses damages your reputation with every hit. Lettr’s suppression list handles this automatically, but you should also maintain your own internal suppression logic.
Ignoring rate limit responses
Treating 429 responses as permanent failures instead of implementing retry logic with backoff. Rate limits are temporary — wait and retry.
No monitoring during the send
Launching a bulk send and walking away. Without real-time monitoring, you won’t catch deliverability problems until it’s too late.
Using individual API calls for each recipient
Sending 50,000 individual API requests when you could batch 200 recipients per request (250 requests total). Batching is dramatically more efficient.