This guide covers deploying the email retry and Dead-Letter Queue (DLQ) feature for the Delego notifications service. The feature adds resilient email delivery with automatic retry on transient failures and persistent tracking of permanently failed emails.
- Review
EMAIL_MAX_RETRIESfor target environment (default: 3, range: 1-10) - Review
EMAIL_RETRY_BASE_DELAY_SECONDSfor target environment (default: 2) - Verify
EMAIL_DLQ_ENABLEDis set appropriately (default: true) - Confirm
SENDGRID_API_KEYis set in target environment - Verify
DATABASE_URLis accessible from service - Verify
LOG_LEVELfor monitoring during initial deployment
- Backup existing database (PostgreSQL)
- Verify database is accessible from service
- Confirm database pool settings (
DATABASE_POOL_MIN,DATABASE_POOL_MAX) - Test connectivity to database from deployment environment
- Verify all npm packages are installed (
pnpm install) - Confirm TypeScript compilation succeeds (
pnpm build) - Run test suite (
pnpm testin notifications package) - Verify code coverage meets 85% threshold
- Deploy to staging environment
- Run database migrations on staging
- Execute end-to-end tests with real SendGrid sandbox
- Verify retry behavior with mocked transient failures
- Monitor logs for configuration validation messages
- Query
failed_notificationstable to verify DLQ functionality
These steps can be done before the feature is activated:
# Install dependencies
pnpm install
# Compile TypeScript
pnpm --filter @delegolabs/notifications build
# Run tests
pnpm --filter @delegolabs/notifications test
# Verify no compilation errors
pnpm --filter @delegolabs/notifications typecheckRun migrations to create the failed_notifications table:
# Apply all pending migrations from the repo root
pnpm db:migrateImportant: This step creates the failed_notifications table. It is idempotent and safe to run multiple times.
Expected migration output:
Database connection established successfully.
Creating failed_notifications table...
Creating indices on (recipient, created_at) and notification_id...
Migration completed successfully.
Deploy the new code to production:
# Build the notifications service
pnpm --filter @delegolabs/notifications build
# Deploy dist/ directory to target environment
# (via your deployment process: Docker, direct push, CI/CD, etc.)Set environment variables and start the notifications service:
# Set configuration variables (add to .env or deployment config)
EMAIL_MAX_RETRIES=3
EMAIL_RETRY_BASE_DELAY_SECONDS=2
EMAIL_DLQ_ENABLED=true
SENDGRID_API_KEY=<your-sendgrid-key>
DATABASE_URL=postgresql://user:pass@host:5432/delego
LOG_LEVEL=info # or debug for more detailed logs during verification
# Start service
pnpm --filter @delegolabs/notifications start
# or via process manager: systemctl start delego-notificationsCheck that the service started correctly:
# Verify logs show configuration loaded
# Expected: "Email retry configuration loaded { EMAIL_MAX_RETRIES: 3, ... }"
# Test email dispatch
curl -X POST http://localhost:3015/notify/transaction-approval \
-H "Content-Type: application/json" \
-d '{"userId":"user-123","email":"user@example.com","transactionId":"txn-123","amount":"100 XLM","merchant":"Example Store","approvalUrl":"https://example.com/approve"}'
# Verify no errors in logs
# Check for: "Database connection established successfully"Monitor the logs for the first hour of production traffic:
# Watch logs for email dispatches
tail -f /var/log/delego/notifications.log
# Expected log patterns:
# - "[notifications:email] DEBUG Email delivered successfully"
# - "[notifications:config] INFO Email retry configuration loaded"
# Alert conditions:
# - "[notifications:dlq] WARN Email moved to Dead-Letter Queue"
# - "[notifications:config] ERROR configuration validation failed"Verify the DLQ is working:
-- Check if failed_notifications table exists
SELECT * FROM failed_notifications LIMIT 1;
-- Count total failed emails (should be minimal)
SELECT COUNT(*) as total_failed_emails FROM failed_notifications;
-- Check recent failures
SELECT recipient, template_name, attempts, error_message, created_at
FROM failed_notifications
WHERE created_at > NOW() - INTERVAL '1 hour'
ORDER BY created_at DESC;If experiencing high failure rates:
# Increase retry attempts for transient-heavy failures
EMAIL_MAX_RETRIES=5
# Increase backoff delay to reduce SendGrid rate limiting
EMAIL_RETRY_BASE_DELAY_SECONDS=3If you want faster retries:
# Decrease retry limit
EMAIL_MAX_RETRIES=2
# Decrease backoff delay (be careful with rate limiting)
EMAIL_RETRY_BASE_DELAY_SECONDS=1If you want to disable DLQ in test environments:
# Disable DLQ logging
EMAIL_DLQ_ENABLED=false
# Use minimal retry for faster test execution
EMAIL_MAX_RETRIES=1
EMAIL_RETRY_BASE_DELAY_SECONDS=0.1-
Email Dispatch Success Rate
-- Query from logs (if structured logging is available) -- Expected: > 95% success rate on first attempt -- Acceptable: > 99% success rate after retries
-
DLQ Creation Rate
SELECT DATE_TRUNC('hour', created_at) as hour, COUNT(*) as dlq_entries FROM failed_notifications GROUP BY DATE_TRUNC('hour', created_at) ORDER BY hour DESC; -- Alert if rate > 1 per minute
-
Retry Success Rate
-- Count emails that required retries (from logs) -- Expected: > 80% of transient failures succeed on retry
-
Average Attempts to Success
-- From logs: distribution of attempts needed -- Expected: Most emails succeed on first attempt (1) -- Some on retry (2-3)
Set up monitoring alerts in your alerting system (DataDog, New Relic, CloudWatch, etc.):
| Alert | Threshold | Action |
|---|---|---|
| DLQ creation rate | > 60/hour | Investigate SendGrid service or email configuration |
| Email dispatch failure rate | > 5% overall | Check SENDGRID_API_KEY validity and rate limits |
| Retry success rate | < 70% | Review error classification or increase EMAIL_MAX_RETRIES |
| Database connection failures | Any | Check DATABASE_URL and network connectivity |
| Configuration validation failed | Any | Check environment variables on startup |
Example logs to look for during verification:
✓ Configuration loaded:
[notifications:config] INFO Email retry configuration loaded {"EMAIL_MAX_RETRIES":3,"EMAIL_RETRY_BASE_DELAY_SECONDS":2,"EMAIL_DLQ_ENABLED":true}
✓ Successful dispatch:
[notifications:email] DEBUG Email delivered successfully {"notificationId":"550e8400...","recipient":"user@example.com","userId":"user-123","attempt":1}
✓ Retry attempt:
[notifications:email] INFO Transient failure detected, retrying {"notificationId":"550e8400...","recipient":"user@example.com","userId":"user-123","failureReason":"ETIMEDOUT","currentAttempt":1,"nextAttemptIn":"2s"}
✓ DLQ entry:
[notifications:dlq] WARN Email moved to Dead-Letter Queue {"notificationId":"550e8400...","recipient":"user@example.com","attempts":3,"errorMessageSummary":"..."}
Symptoms: DLQ row creation rate > 60/hour
Investigation:
-- Find most common error types
SELECT error_message, COUNT(*) as count
FROM failed_notifications
WHERE created_at > NOW() - INTERVAL '1 hour'
GROUP BY error_message
ORDER BY count DESC;
-- Find recipients with multiple failures
SELECT recipient, COUNT(*) as count
FROM failed_notifications
WHERE created_at > NOW() - INTERVAL '1 hour'
GROUP BY recipient
ORDER BY count DESC;Common Causes:
- Invalid email addresses: Review recipient validation
- SendGrid rate limit: Increase
EMAIL_RETRY_BASE_DELAY_SECONDSor reduce traffic - SendGrid API key issues: Verify SENDGRID_API_KEY is valid and has sufficient quota
- Network issues: Check database and service connectivity
Resolution:
- Invalid emails: Fix recipient list before re-sending
- Rate limiting: Adjust backoff strategy or contact SendGrid about limits
- API key: Verify key and quota in SendGrid dashboard
- Network: Verify connectivity and database pool settings
Symptoms: Low attempt count in DLQ entries (all show attempts: 1)
Investigation:
-- Check if retries are being attempted
SELECT attempts, COUNT(*) as count
FROM failed_notifications
WHERE created_at > NOW() - INTERVAL '1 hour'
GROUP BY attempts;
-- Should show attempts: 2, 3 for some entries (indicating retries)Common Causes:
- Error classification too aggressive: Permanent failures classified incorrectly
- EMAIL_MAX_RETRIES set to 0 or 1
- Log level too low to see retry attempts
Resolution:
- Review error classification logic for false positives
- Increase EMAIL_MAX_RETRIES to at least 2
- Set LOG_LEVEL=debug temporarily to see retry details
Symptoms: Email delivery takes 30+ seconds
Investigation:
- Check logs for retry delay calculations
- Monitor database query times
- Check SendGrid API response times
Common Causes:
EMAIL_RETRY_BASE_DELAY_SECONDStoo high- Database connection pool exhausted
- SendGrid rate limiting causing slow responses
Resolution:
- Reduce backoff delay if appropriate for your traffic
- Increase DATABASE_POOL_MAX
- Review SendGrid rate limits and request distribution
Symptoms: Errors in logs like "Database connection failed"
Investigation:
# Test database connectivity
psql -h $DATABASE_HOST -U $DATABASE_USER -d $DATABASE_NAME -c "SELECT NOW();"
# Check database pool status
# From application logs or monitoring dashboardCommon Causes:
- Incorrect DATABASE_URL format
- Database server not accessible from service
- Database pool exhausted
Resolution:
- Verify DATABASE_URL:
postgresql://user:pass@host:port/dbname - Check firewall rules and network connectivity
- Increase DATABASE_POOL_MAX or reduce connection usage
- Restart connection pool: restart service (non-breaking)
# 1. Revert code to previous version
git revert <current-commit>
pnpm build
# Deploy previous version
# 2. The DLQ table remains (data is preserved)
# Future dispatches use new code
# Investigation can continue on existing DLQ entriesIf only DLQ logging is problematic:
# Set environment variable
EMAIL_DLQ_ENABLED=false
# Restart service
# Email retry will continue, but won't log to DLQ
# Re-enable after investigationIf configuration values are problematic:
# Revert to safer defaults
EMAIL_MAX_RETRIES=2
EMAIL_RETRY_BASE_DELAY_SECONDS=5
EMAIL_DLQ_ENABLED=true
# Restart service
# Existing DLQ entries remain for investigationAfter rollback, verify:
# Check service health
curl http://localhost:3015/health
# Verify emails are dispatching
# Check logs for dispatch activity
# Query DLQ to confirm data preserved
SELECT COUNT(*) FROM failed_notifications;
# Confirm no new DLQ entries if DLQ is disabled
SELECT created_at FROM failed_notifications
ORDER BY created_at DESC LIMIT 5;-- Find all failed emails for a specific user
SELECT * FROM failed_notifications
WHERE payload->>'userId' = 'user-123'
ORDER BY created_at DESC;
-- Find emails that failed due to timeout
SELECT * FROM failed_notifications
WHERE error_message ILIKE '%timeout%'
ORDER BY created_at DESC;
-- Find emails that failed in the last 24 hours
SELECT * FROM failed_notifications
WHERE created_at > NOW() - INTERVAL '24 hours'
ORDER BY created_at DESC;-- Count DLQ entries in last hour
SELECT COUNT(*) as dlq_entries_1h
FROM failed_notifications
WHERE created_at > NOW() - INTERVAL '1 hour';
-- Find recipients with repeated failures
SELECT recipient, COUNT(*) as failure_count, MAX(created_at) as latest
FROM failed_notifications
WHERE created_at > NOW() - INTERVAL '24 hours'
GROUP BY recipient
HAVING COUNT(*) > 2
ORDER BY failure_count DESC;-- Archive old DLQ entries (optional, 90+ day retention)
-- Create archive table first, then delete
DELETE FROM failed_notifications
WHERE created_at < NOW() - INTERVAL '90 days';
-- Or select into archive
SELECT * INTO failed_notifications_archive_2024_q1
FROM failed_notifications
WHERE created_at BETWEEN '2024-01-01' AND '2024-03-31';
DELETE FROM failed_notifications
WHERE created_at BETWEEN '2024-01-01' AND '2024-03-31';The migration creates two indices automatically:
-- Indices created by migration
CREATE INDEX idx_failed_notifications_recipient_created_at
ON failed_notifications(recipient, created_at);
CREATE INDEX idx_failed_notifications_notification_id
ON failed_notifications(notification_id);These enable efficient queries:
- By recipient + time range (operator investigation)
- By notification ID (quick lookup)
Configure connection pool based on expected load:
# Default settings (conservative)
DATABASE_POOL_MIN=2
DATABASE_POOL_MAX=10
# For high-volume environments
DATABASE_POOL_MIN=5
DATABASE_POOL_MAX=20
# For low-traffic environments
DATABASE_POOL_MIN=1
DATABASE_POOL_MAX=5Email retry logic is minimal overhead:
- Each retry uses exponential backoff (memory-efficient)
- DLQ records are stored in database (not in-memory)
- Typical memory increase: < 5MB for the feature
- Check configuration validation logs
- Verify database connectivity
- Run diagnostic tests
- Consult troubleshooting section above
- Gather logs:
tail -1000 /var/log/delego/notifications.log - Query DLQ for error patterns
- Review configuration settings
- Check SendGrid API status and quota
- Verify database connectivity and health
After deployment, verify:
- Service starts without configuration errors
- At least one email successfully dispatched (appears in logs)
-
failed_notificationstable exists with correct schema - No errors in initial deployment logs
- DLQ queries return results for test emails
- Email retry configuration logged at startup
- Email dispatch success rate > 95% within first hour