Sending 1 Million Emails in 25 Minutes
High-Performance Mautic on AWS
The client operates a SaaS platform with white-labeled marketing automation powered by Mautic. As the platform grew to support hundreds of tenants sending campaigns simultaneously, the default Mautic email sending pipeline — processing contacts one-by-one — became a critical bottleneck. We needed to fundamentally rethink how emails are batched, queued, and dispatched.
Performance Results
| Emails sent | 1,000,000 |
| Time | 25 minutes |
| Throughput | ~40,000 emails/minute |
| Parallel workers | ~52 during stress test |
| Batch size | 1,000 recipients per Mailgun API call |
| Queue stages | 2 (segment scan → payload assembly → dispatch) |
| Stage 1 duration | Seconds (ID batching only) |
| Personalization | Per-recipient dynamic content (branch, location, images) |
The Challenge
The Pipeline
Milliseconds to batch millions
Fetches the full contact list, creates lightweight batches of contact IDs, and pushes them to the first SQS queue. Completes in seconds — even for millions of contacts.
52 parallel workers
Workers pull contact ID batches, load full contact data, resolve personalization tokens, build Mailgun payloads. Each worker handles roughly one batch per minute.
1,000 recipients per API call
Mailgun receives a template with placeholder tokens and up to 1,000 recipients per call. A batch of 1,000 emails becomes a single API request — not 1,000 separate requests.
Milliseconds to batch millions
Fetches the full contact list, creates lightweight batches of contact IDs, and pushes them to the first SQS queue. Completes in seconds — even for millions of contacts.
52 parallel workers
Workers pull contact ID batches, load full contact data, resolve personalization tokens, build Mailgun payloads. Each worker handles roughly one batch per minute.
1,000 recipients per API call
Mailgun receives a template with placeholder tokens and up to 1,000 recipients per call. A batch of 1,000 emails becomes a single API request — not 1,000 separate requests.
Engineering Challenges We Solved
Custom SQS + S3 Transport
AWS SQS has a strict 256 KB payload limit. Email batch payloads with 1,000 recipients easily exceed this. Our solution:
Built as a custom transport based on the Symfony Messenger SQS Bridge.
Intelligent Bounce Handling
All tenants share a single Mailgun sending domain. Bounce notifications need to reach the correct tenant. Rather than unreliable webhooks, we built a crawler-based approach:
- Periodically polls the Mailgun API for bounce records
- Each email includes a custom header with the tenant identifier
- Routes bounce data to the correct Mautic instance
- Handles both hard bounces and soft bounces
Graceful Processing Under Spot Interruptions
Workers run on AWS spot instances for cost efficiency, but spot instances can be reclaimed with only minutes' notice. Our safeguards:
- Graceful shutdown signals — workers get 1-2 min to finish current batch
- Proper SQS connection cleanup prevents ambiguous message states
- Force kill fallback returns unprocessed messages to queue
Email Deduplication & Queue Priority
Real-time deduplication ensures only one email per unique address within a campaign — even across batches processed by different workers. Queue priority rules ensure fully assembled batches are always dispatched first, keeping the pipeline flowing under heavy load from simultaneous campaigns.
Business Impact
Technology Stack
Need High-Performance Email at Scale?
Let's discuss your email infrastructure and performance goals.