Benchmarks & the real bottleneck
Every number below is from a real run of the delivered code — dry-run transport simulating an 80ms bulk API with ±30% jitter, ~1% transient failures.
Cumulative throughput across the 1M-recipient run
The ramp is the pipeline filling its 20 in-flight slots; the plateau is steady state, where a new batch launches the instant any slot frees. Small dips line up with retry backoffs (14 across the run).
View as table
| % complete | cumulative emails/s |
|---|
Why batching wins: the request count collapses
A bulk ESP request carries one shared template plus 1,000 recipient personalizations — the provider fans out delivery on its side.
Rate is a separate control from concurrency
The semaphore bounds work in flight (our memory and sockets); the token bucket bounds request starts/sec (the provider's quota). With only the semaphore, effective rate is concurrency ÷ provider latency — a number the provider controls — and 429-retry becomes the flow control. Measured: 200k recipients against a simulated 50 req/s quota.
| Configuration | Wall clock | 429s (wasted round trips) | Total requests |
|---|---|---|---|
Semaphore only — no --rate | 4.2 s | 75 | 276 |
Token bucket — --rate 50two gates | 4.2 s | 3 | 203 |
Five approaches, one honest bottleneck
Durations span five orders of magnitude, so a table is the honest form — bars on a log axis would visually flatten a 20,000× difference.
| Approach | Requests for 1M | Wall clock | What actually limits it |
|---|---|---|---|
| 1 · Sequential loop | 1,000,000 | ~42 h | Round-trip latency, paid serially |
| 2 · Bounded concurrency, individual | 1,000,000 | ~25 min | Provider rate limits — 429s long before your network saturates |
| 3 · Bulk API + batchingchosen | ~1,000 | 7.4 s measured* | Your provisioned ESP quota — the only unavoidable limit |
| 4 · Raw SMTP, pooled | 1,000,000 msgs | — | IP reputation: greylisting, tarpits, blackholes |
| 5 · Queue + workers | ~1,000 | seconds–min | Same as 3, plus a Redis to operate |
Concurrency sweep: the client ceiling vs. the quota wall
200k recipients per config, one run each. Unlimited: throughput scales near-ideally to c=20, then the dispatch side (CSV parsing + per-batch CPU) caps the client at ~330k emails/s. Under a 50 req/s quota with no client limiter: throughput pins at the quota from c=10 while latency degrades — and past saturation it's the median that collapses, not just the tail.
Throughput (emails/s)
Batch completion latency, p99 (ms)
View as table
| Regime | c | Wall | Emails/s | p50 | p95 | p99 | 429s |
|---|