Files
2026-01-24 20:22:18 +00:00

156 lines
4.7 KiB
Markdown

# Sending Reliability
Ensuring emails are sent exactly once and handling failures gracefully.
## Idempotency
Prevent duplicate emails when retrying failed requests.
### The Problem
Network issues, timeouts, or server errors can leave you uncertain if an email was sent. Retrying without idempotency risks sending duplicates.
### Solution: Idempotency Keys
Send a unique key with each request. If the same key is sent again, the server returns the original response instead of sending another email.
```typescript
// Generate deterministic key based on the business event
const idempotencyKey = `password-reset-${userId}-${resetRequestId}`;
await resend.emails.send({
from: 'noreply@example.com',
to: user.email,
subject: 'Reset your password',
html: emailHtml,
}, {
headers: {
'Idempotency-Key': idempotencyKey
}
});
```
### Key Generation Strategies
| Strategy | Example | Use When |
|----------|---------|----------|
| Event-based | `order-confirm-${orderId}` | One email per event (recommended) |
| Request-scoped | `reset-${userId}-${resetRequestId}` | Retries within same request |
| UUID | `crypto.randomUUID()` | No natural key (generate once, reuse on retry) |
**Best practice:** Use deterministic keys based on the business event. If you retry the same logical send, the same key must be generated. Avoid `Date.now()` or random values generated fresh on each attempt.
**Key expiration:** Idempotency keys are typically cached for 24 hours. Retries within this window return the original response. After expiration, the same key triggers a new send—so complete your retry logic well within 24 hours.
## Retry Logic
Handle transient failures with exponential backoff.
### When to Retry
| Error Type | Retry? | Notes |
|------------|--------|-------|
| 5xx (server error) | ✅ Yes | Transient, likely to resolve |
| 429 (rate limit) | ✅ Yes | Wait for rate limit window |
| 4xx (client error) | ❌ No | Fix the request first |
| Network timeout | ✅ Yes | Transient |
| DNS failure | ✅ Yes | May be transient |
### Exponential Backoff
```typescript
async function sendWithRetry(emailData, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await resend.emails.send(emailData);
} catch (error) {
if (!isRetryable(error) || attempt === maxRetries - 1) {
throw error;
}
const delay = Math.min(1000 * Math.pow(2, attempt), 30000);
await sleep(delay + Math.random() * 1000); // Add jitter
}
}
}
function isRetryable(error) {
return error.statusCode >= 500 ||
error.statusCode === 429 ||
error.code === 'ETIMEDOUT';
}
```
**Backoff schedule:** 1s → 2s → 4s → 8s (with jitter to prevent thundering herd)
## Error Handling
### Common Error Codes
| Code | Meaning | Action |
|------|---------|--------|
| 400 | Bad request | Fix payload (invalid email, missing field) |
| 401 | Unauthorized | Check API key |
| 403 | Forbidden | Check permissions, domain verification |
| 404 | Not found | Check endpoint URL |
| 422 | Validation error | Fix request data |
| 429 | Rate limited | Back off, retry after delay |
| 500 | Server error | Retry with backoff |
| 503 | Service unavailable | Retry with backoff |
### Error Handling Pattern
```typescript
try {
const result = await resend.emails.send(emailData);
await logSuccess(result.id, emailData);
} catch (error) {
if (error.statusCode === 429) {
await queueForRetry(emailData, error.retryAfter);
} else if (error.statusCode >= 500) {
await queueForRetry(emailData);
} else {
await logFailure(error, emailData);
await alertOnCriticalEmail(emailData); // For password resets, etc.
}
}
```
## Queuing for Reliability
For critical emails, use a queue to ensure delivery even if the initial send fails.
**Benefits:**
- Survives application restarts
- Automatic retry handling
- Rate limit management
- Audit trail
**Simple pattern:**
1. Write email to queue/database with "pending" status
2. Process queue, attempt send
3. On success: mark "sent", store message ID
4. On retryable failure: increment retry count, schedule retry
5. On permanent failure: mark "failed", alert
## Timeouts
Set appropriate timeouts to avoid hanging requests.
```typescript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000);
try {
await resend.emails.send(emailData, { signal: controller.signal });
} finally {
clearTimeout(timeout);
}
```
**Recommended:** 10-30 seconds for email API calls.
## Related
- [Webhooks & Events](./webhooks-events.md) - Process delivery confirmations and failures
- [List Management](./list-management.md) - Handle bounces and suppress invalid addresses