What breaks when you send 50,000 emails a day
Sending scale is rarely limited by the mail provider. It is limited by queueing, rate pacing and the parts of your system that assumed one email at a time.
Sending one email is a function call. Sending fifty thousand is a distributed systems problem wearing the same interface.
Rate limits are the real ceiling
Providers cap two things: how many you may send per day, and how many per second. The per-second limit is the one that shapes your code.
At 14 sends a second, 50,000 messages takes roughly an hour of continuous sending. Fire them in a loop as fast as your process allows and you will be throttled within seconds, then have to work out which ones actually went.
Pace deliberately, and pace against the limit the provider reports rather than one you hardcoded — those limits rise as your reputation improves, and a hardcoded number quietly caps you at a fraction of what you are paying for.
Claim work before doing it
The naive worker reads pending recipients and sends to them. Run two copies and everyone gets two emails.
Claim atomically instead:
UPDATE campaign_recipients
SET status = 'sending', claim_id = ?, claimed_at = NOW()
WHERE campaign_id = ? AND status = 'pending'
LIMIT 250
Then read back the rows carrying your claim ID. Two workers cannot claim the same row, because the status = 'pending' condition only matches once.
This also survives crashes: rows stuck in sending past a timeout get released back to pending. Without that sweep, a worker that dies mid-batch strands its recipients permanently.
Everything asynchronous arrives later
Bounces, complaints, opens and clicks do not come back in the send response. They arrive by webhook, minutes or hours afterwards, referencing a provider message ID.
Two consequences. First, store that message ID at send time — it is the only way to connect an event to a send. Second, index it. That column is read on every incoming event, and at 50,000 sends a day the table passes a million rows within a month. Unindexed, each event becomes a full table scan.
Batch your bookkeeping
It is tempting to update a usage counter per message. At 50,000 a day that is 50,000 extra writes for a number that only has to be right in aggregate.
Count in memory, write once per batch.
Guard against the run that never ends
A send loop should have a deadline and a budget. When either runs out, release unsent claims and stop.
Without that, a campaign to a list larger than expected occupies your worker indefinitely, and every other campaign waits behind it.
The unglamorous truth
Most of this is not about email. It is idempotency, atomic claiming, bounded concurrency and asynchronous reconciliation — the same problems as any queue.
Which is the useful insight: treat sending as a job queue that happens to produce email, and the scaling questions have well-known answers.