278 lines
6.2 KiB
Plaintext
278 lines
6.2 KiB
Plaintext
---
|
|
title: Scaling Email Delivery
|
|
description: Best practices for high-volume email sending
|
|
---
|
|
|
|
## Email delivery at scale
|
|
|
|
Plunk is built on AWS SES and handles millions of emails. Follow these practices to maintain high deliverability and performance at scale.
|
|
|
|
## Deliverability best practices
|
|
|
|
### Use custom domains
|
|
|
|
Emails from custom domains have higher trust and better deliverability than shared domains.
|
|
|
|
**Setup:**
|
|
1. Go to **Settings > Domains**
|
|
2. Add your domain
|
|
3. Configure DNS records (DKIM, SPF)
|
|
4. Wait for verification
|
|
|
|
[Learn more about custom domains →](/guides/custom-domains)
|
|
|
|
### Warm up new domains
|
|
|
|
Start small and gradually increase volume.
|
|
|
|
This builds sender reputation with email providers.
|
|
|
|
### Clean your list regularly
|
|
|
|
Remove bounced and inactive contacts:
|
|
|
|
```javascript
|
|
// Get bounced contacts
|
|
const bounced = await fetch('{{API_URL}}/events?name=email.bounced&limit=1000', {
|
|
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
});
|
|
|
|
// Unsubscribe them
|
|
for (const event of bounced.data.events) {
|
|
await fetch(`{{API_URL}}/contacts/${event.contactId}`, {
|
|
method: 'PATCH',
|
|
headers: {
|
|
'Authorization': `Bearer ${apiKey}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ subscribed: false })
|
|
});
|
|
}
|
|
```
|
|
|
|
**When to clean:**
|
|
- Hard bounces: Immediately unsubscribe
|
|
- Soft bounces: After 3 attempts
|
|
- No engagement: After 6-12 months
|
|
|
|
### Segment your audience
|
|
|
|
Send relevant content to engaged users:
|
|
|
|
```javascript
|
|
// Create engaged users segment
|
|
{
|
|
"name": "Engaged Users",
|
|
"filters": {
|
|
"operator": "OR",
|
|
"conditions": [
|
|
{ "field": "data.lastOpenedAt", "operator": "greaterThan", "value": "{{90 days ago}}" },
|
|
{ "field": "data.lastClickedAt", "operator": "greaterThan", "value": "{{90 days ago}}" }
|
|
]
|
|
}
|
|
}
|
|
```
|
|
|
|
Send campaigns to engaged segments for better rates.
|
|
|
|
## Tracking control
|
|
|
|
You can disable tracking in your project settings for privacy-focused audiences or to reduce email size.
|
|
|
|
**When tracking is disabled:**
|
|
- No tracking pixel (no open tracking)
|
|
- Links not rewritten (no click tracking)
|
|
- Smaller email size
|
|
- May improve deliverability for privacy-conscious audiences
|
|
|
|
**When to disable:**
|
|
- Regulated industries (healthcare, finance)
|
|
- Privacy-focused users
|
|
- Transactional emails where tracking isn't needed
|
|
- High-volume sends where analytics aren't critical
|
|
|
|
## Rate limits
|
|
|
|
### AWS SES limits
|
|
|
|
Plunk automatically queues emails to stay within limits. Large sends process in background.
|
|
|
|
### Increase limits
|
|
|
|
For higher throughput:
|
|
1. Maintain good sender reputation
|
|
2. Consistent sending volume
|
|
3. Low bounce/complaint rates
|
|
4. Request limit increase from AWS
|
|
|
|
### Batch operations
|
|
|
|
For bulk operations, use appropriate endpoints:
|
|
|
|
```javascript
|
|
// ✓ Good: Single request for multiple recipients
|
|
fetch('{{API_URL}}/v1/send', {
|
|
method: 'POST',
|
|
body: JSON.stringify({
|
|
to: ['user1@example.com', 'user2@example.com', 'user3@example.com'],
|
|
subject: 'Update',
|
|
body: 'Message'
|
|
})
|
|
});
|
|
|
|
// ✗ Avoid: Multiple requests
|
|
for (const email of emails) {
|
|
await fetch('{{API_URL}}/v1/send', {...}); // Sequential, slow
|
|
}
|
|
```
|
|
|
|
## Campaign targeting
|
|
|
|
### Dynamic filtering
|
|
|
|
Target audiences without creating segments:
|
|
|
|
```javascript
|
|
{
|
|
"name": "Premium Launch",
|
|
"audienceType": "FILTERED",
|
|
"audienceFilter": {
|
|
"operator": "AND",
|
|
"conditions": [
|
|
{ "field": "data.plan", "operator": "equals", "value": "premium" },
|
|
{ "field": "data.signupDate", "operator": "greaterThan", "value": "2025-01-01" },
|
|
{ "field": "subscribed", "operator": "equals", "value": true }
|
|
]
|
|
},
|
|
"templateId": "template_id"
|
|
}
|
|
```
|
|
|
|
**Use FILTERED for:**
|
|
- One-time sends
|
|
- Testing targeting
|
|
- Very specific criteria
|
|
|
|
**Use SEGMENT for:**
|
|
- Repeated targeting
|
|
- Workflow triggers
|
|
- Segment analytics
|
|
|
|
## Segment membership tracking
|
|
|
|
For segments used in workflows, enable membership tracking:
|
|
|
|
```bash
|
|
curl -X POST {{API_URL}}/segments \
|
|
-H "Authorization: Bearer sk_your_secret_key" \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"name": "Active Premium Users",
|
|
"filters": {...},
|
|
"trackMembership": true
|
|
}'
|
|
```
|
|
|
|
**Enable for:**
|
|
- Workflow trigger segments
|
|
- Lifecycle stage tracking
|
|
- Cohort analysis
|
|
|
|
**Disable for:**
|
|
- Large segments (100k+ contacts)
|
|
- Campaign-only segments
|
|
- Frequently changing segments
|
|
|
|
Membership updates run every 5 minutes in background.
|
|
|
|
## Performance optimization
|
|
|
|
### Cache contact data
|
|
|
|
For high-volume API sends, cache contact lookups:
|
|
|
|
```javascript
|
|
// Cache contact IDs locally
|
|
const contactCache = new Map();
|
|
|
|
async function getContactId(email) {
|
|
if (contactCache.has(email)) {
|
|
return contactCache.get(email);
|
|
}
|
|
|
|
const contact = await fetch(`{{API_URL}}/contacts?search=${email}`);
|
|
contactCache.set(email, contact.id);
|
|
return contact.id;
|
|
}
|
|
```
|
|
|
|
### Use webhooks for async processing
|
|
|
|
Instead of waiting for email sends:
|
|
|
|
```javascript
|
|
// Workflow with webhook for confirmation
|
|
{
|
|
"steps": [
|
|
{ "type": "SEND_EMAIL", "config": {...} },
|
|
{
|
|
"type": "WEBHOOK",
|
|
"config": {
|
|
"url": "https://your-api.com/email-sent",
|
|
"method": "POST",
|
|
"body": {
|
|
"contactId": "{{id}}",
|
|
"emailId": "{{emailId}}"
|
|
}
|
|
}
|
|
}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Batch workflow triggers
|
|
|
|
Trigger workflows in batches instead of one at a time:
|
|
|
|
```javascript
|
|
// Batch event tracking
|
|
const events = users.map(user => ({
|
|
email: user.email,
|
|
event: 'welcome_campaign',
|
|
data: { userId: user.id }
|
|
}));
|
|
|
|
// Send in parallel (respecting rate limits)
|
|
await Promise.all(
|
|
events.map(event =>
|
|
fetch('{{API_URL}}/v1/track', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${apiKey}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(event)
|
|
})
|
|
)
|
|
);
|
|
```
|
|
|
|
## Monitoring
|
|
|
|
### Track key metrics
|
|
|
|
Monitor these regularly:
|
|
|
|
- **Bounce rate** — Should be < 2%
|
|
- **Complaint rate** — Should be < 0.1%
|
|
- **Open rate** — Industry average 15-25%
|
|
- **Click rate** — Industry average 2-5%
|
|
|
|
Monitor these metrics in your dashboard analytics page to track deliverability and engagement over time.
|
|
|
|
## Next Steps
|
|
|
|
- [Set billing limits](/guides/billing-limits) to control costs
|
|
- [Monitor analytics](/guides/analytics) for deliverability
|
|
- [Troubleshoot issues](/guides/troubleshooting) if problems arise
|