[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-0ff7c856-51c6-4430-9a20-91eb18ae4981":3},{"id":4,"body":5,"uuid":6,"created_at":7,"updated_at":8,"brand":9,"header":10,"short_body":11,"image":12,"published":13,"published_at":14,"tags":15},21,"\u003Cp>To \u003Cstrong>send millions of emails\u003C\u002Fstrong> per month reliably, you need four things working together: a durable queue that decouples your application from your email provider, rate limiting that respects each provider and ISP, controlled concurrency across workers, and monitoring that catches reputation problems before they cascade. Get those right and a million messages a month — roughly 33,000 a day, or about 23 emails per minute averaged out — is a routine workload, not a crisis.\u003C\u002Fp>\n\u003Cp>The hard part is rarely the raw send volume. One million emails a month is a modest number for any serious mail platform; providers handle billions. The hard part is doing it \u003Cem>without your code falling over during traffic spikes, without burning your sender reputation, and without losing messages when something downstream fails\u003C\u002Fem>. That requires deliberate \u003Cstrong>scalable email infrastructure\u003C\u002Fstrong> rather than a \u003Ccode>for\u003C\u002Fcode> loop that calls an API.\u003C\u002Fp>\n\u003Cp>This guide is a practical, architecture-first walkthrough for engineers and CTOs who need to send millions of emails reliably. We'll cover queueing, rate limits, concurrency, multiple domains and IPs, retries, monitoring, and what it actually costs at scale — with text-described architecture diagrams and runnable code examples. By the end you'll have a concrete blueprint you can implement incrementally.\u003C\u002Fp>\n\u003Ch2>What \"1 Million Emails per Month\" Actually Means\u003C\u002Fh2>\n\u003Cp>Before designing anything, translate the headline number into rates, because rates — not totals — are what break systems.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Metric\u003C\u002Fth>\n\u003Cth>Value (even distribution)\u003C\u002Fth>\n\u003Cth>Realistic peak (10x burst)\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Per month\u003C\u002Ftd>\n\u003Ctd>1,000,000\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Per day\u003C\u002Ftd>\n\u003Ctd>~33,300\u003C\u002Ftd>\n\u003Ctd>—\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Per hour\u003C\u002Ftd>\n\u003Ctd>~1,390\u003C\u002Ftd>\n\u003Ctd>~13,900\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Per minute\u003C\u002Ftd>\n\u003Ctd>~23\u003C\u002Ftd>\n\u003Ctd>~230\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Per second\u003C\u002Ftd>\n\u003Ctd>~0.4\u003C\u002Ftd>\n\u003Ctd>~4\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Two lessons fall out of this table immediately.\u003C\u002Fp>\n\u003Cp>First, the \u003Cstrong>average\u003C\u002Fstrong> rate is tiny — well under one email per second. If your traffic were perfectly smooth, a single thread could handle it. It never is.\u003C\u002Fp>\n\u003Cp>Second, the \u003Cstrong>peak\u003C\u002Fstrong> is what you must design for. Real transactional traffic is bursty: a product launch, a billing run, a \"your trial ends today\" campaign, or a 9 a.m. wave of password resets can push you to 10x or 50x the average for short windows. Scalable email infrastructure is fundamentally about absorbing those bursts gracefully — accepting work instantly and draining it at a sustainable rate.\u003C\u002Fp>\n\u003Cp>This is the core mental shift: \u003Cstrong>decouple acceptance from delivery.\u003C\u002Fstrong> Your application should hand off an email in microseconds and move on. A separate system delivers it at whatever rate is safe.\u003C\u002Fp>\n\u003Ch2>The Architecture to Send Millions of Emails\u003C\u002Fh2>\n\u003Cp>Here is the reference architecture, described as a diagram in text:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>[ Your App \u002F API ]\n        |\n        v  (enqueue: fast, non-blocking)\n[ Durable Message Queue ]  &lt;-- Redis \u002F RabbitMQ \u002F SQS \u002F Kafka\n        |\n        v  (pull work)\n[ Pool of Email Workers ]  --- concurrency N, rate-limited ---+\n        |                                                     |\n        v                                                     v\n[ Email Provider \u002F SMTP relay ]                        [ Retry \u002F DLQ ]\n        |\n        v  (async webhooks: delivered, bounced, complained)\n[ Event Consumer ] --&gt; [ Metrics + Suppression DB ] --&gt; [ Alerting ]\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The flow, stage by stage:\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Application\u003C\u002Fstrong> validates and enqueues a message. It does \u003Cem>not\u003C\u002Fem> call the email provider directly. This call must be fast and must not block a user request.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Durable queue\u003C\u002Fstrong> persists the message so it survives a crash. This is the buffer that absorbs bursts.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Worker pool\u003C\u002Fstrong> pulls messages, applies rate limiting and concurrency control, and calls the provider. Workers scale horizontally.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Email provider \u002F SMTP relay\u003C\u002Fstrong> accepts the message and handles the actual SMTP delivery to recipient mail servers.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Retry \u002F dead-letter queue (DLQ)\u003C\u002Fstrong> captures transient failures for backoff retries and permanent failures for inspection.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Event consumer\u003C\u002Fstrong> ingests delivery\u002Fbounce\u002Fcomplaint webhooks, updates metrics, and feeds the suppression list.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Alerting\u003C\u002Fstrong> watches the metrics and pages you before a reputation problem becomes an outage.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>Every section below is one piece of this picture.\u003C\u002Fp>\n\u003Ch2>Queueing: The Foundation of Scalable Email Infrastructure\u003C\u002Fh2>\n\u003Cp>A queue is non-negotiable to \u003Cstrong>send millions of emails\u003C\u002Fstrong>. Without one, every email send is coupled to a live HTTP request to your provider — so a provider slowdown becomes a user-facing slowdown, a crash loses in-flight messages, and a burst overruns your rate limits instantly.\u003C\u002Fp>\n\u003Ch3>Why a queue first\u003C\u002Fh3>\n\u003Cp>A durable queue gives you four properties for free:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Burst absorption\u003C\u002Fstrong> — accept 10,000 emails in a second, deliver them over the next ten minutes.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Durability\u003C\u002Fstrong> — messages survive worker crashes and deploys; nothing is lost in memory.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Backpressure\u003C\u002Fstrong> — when downstream is slow, work accumulates safely instead of failing.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Decoupling\u003C\u002Fstrong> — your web tier and your delivery tier scale independently.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Choosing a queue\u003C\u002Fh3>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Queue\u003C\u002Fth>\n\u003Cth>Best for\u003C\u002Fth>\n\u003Cth>Throughput\u003C\u002Fth>\n\u003Cth>Notes\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Redis (RQ\u002FBullMQ\u002FSidekiq)\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Most SaaS at this scale\u003C\u002Ftd>\n\u003Ctd>Very high\u003C\u002Ftd>\n\u003Ctd>Simple, fast, low ops; pair with persistence\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>RabbitMQ\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Complex routing, priorities\u003C\u002Ftd>\n\u003Ctd>High\u003C\u002Ftd>\n\u003Ctd>Mature, flexible, more to operate\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>AWS SQS\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Serverless \u002F AWS-native\u003C\u002Ftd>\n\u003Ctd>High\u003C\u002Ftd>\n\u003Ctd>Fully managed, built-in DLQ and visibility timeout\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Kafka\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Huge volume, event streaming\u003C\u002Ftd>\n\u003Ctd>Extreme\u003C\u002Ftd>\n\u003Ctd>Overkill for 1M\u002Fmonth; great at 1B\u002Fmonth\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>For one million emails a month, \u003Cstrong>Redis-backed queues or SQS are the sweet spot.\u003C\u002Fstrong> Kafka is over-engineering until you're an order of magnitude larger. Pick the boring option your team already runs.\u003C\u002Fp>\n\u003Ch3>A minimal producer\u002Fconsumer in Python\u003C\u002Fh3>\n\u003Cp>The producer enqueues without ever touching the email API:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\">import json\nimport redis\n\nr = redis.Redis(host=&quot;localhost&quot;, port=6379, db=0)\nQUEUE_KEY = &quot;email:outbound&quot;\n\n\ndef enqueue_email(to: str, template: str, context: dict) -&gt; None:\n    &quot;&quot;&quot;Called from your app. Fast, non-blocking, never sends directly.&quot;&quot;&quot;\n    job = {&quot;to&quot;: to, &quot;template&quot;: template, &quot;context&quot;: context}\n    r.lpush(QUEUE_KEY, json.dumps(job))\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The worker pulls and sends, with the rate limiting and retries we add below:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\">import json\nimport time\nimport redis\nimport requests\n\nr = redis.Redis(host=&quot;localhost&quot;, port=6379, db=0)\nQUEUE_KEY = &quot;email:outbound&quot;\nDLQ_KEY = &quot;email:dead&quot;\nPROVIDER_URL = &quot;https:\u002F\u002Fapi.postwing.app\u002Fv1\u002Fsend&quot;\nAPI_KEY = &quot;pw_live_xxx&quot;\n\n\ndef send_via_provider(job: dict) -&gt; requests.Response:\n    return requests.post(\n        PROVIDER_URL,\n        headers={&quot;Authorization&quot;: f&quot;Bearer {API_KEY}&quot;},\n        json=job,\n        timeout=15,\n    )\n\n\ndef worker_loop() -&gt; None:\n    while True:\n        item = r.brpop(QUEUE_KEY, timeout=5)\n        if item is None:\n            continue\n        _, raw = item\n        job = json.loads(raw)\n        try:\n            resp = send_via_provider(job)\n            resp.raise_for_status()\n        except requests.RequestException:\n            r.lpush(DLQ_KEY, raw)  # retry logic refined later\n        time.sleep(0.0)  # rate limiting added below\n\n\nif __name__ == &quot;__main__&quot;:\n    worker_loop()\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>This is the skeleton. The next sections turn it into something safe at scale.\u003C\u002Fp>\n\u003Ch2>Rate Limits: Respect the Provider and the ISPs\u003C\u002Fh2>\n\u003Cp>Rate limits exist at two layers, and you must respect both to send millions of emails without getting throttled or blocked.\u003C\u002Fp>\n\u003Col>\n\u003Cli>\u003Cstrong>Provider rate limits\u003C\u002Fstrong> — your email API enforces a requests-per-second cap (often by plan). Exceed it and you get \u003Ccode>429 Too Many Requests\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>ISP rate limits\u003C\u002Fstrong> — Gmail, Outlook, Yahoo and others throttle \u003Cem>per sending domain\u002FIP reputation\u003C\u002Fem>. Send too fast from a cold or low-reputation sender and they defer or reject you, regardless of what your provider allows.\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>The second layer is the subtle one. A provider may happily accept 500 requests\u002Fsecond from you, but Gmail will start deferring (\u003Ccode>4xx\u003C\u002Fcode>) your mail if a new IP suddenly blasts it. \u003Cstrong>Throughput is gated by reputation, not just by API limits.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch3>Implementing a token-bucket rate limiter\u003C\u002Fh3>\n\u003Cp>A token bucket is the standard, simple algorithm: tokens refill at a steady rate; each send consumes one; when the bucket is empty, senders wait. Here's a distributed version using Redis so it works across many worker processes:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\">import time\nimport redis\n\nr = redis.Redis(host=&quot;localhost&quot;, port=6379, db=0)\n\n# Refill 20 tokens\u002Fsec, bucket capacity 40 (allows short bursts).\nRATE = 20.0\nCAPACITY = 40\n\n_TOKEN_BUCKET_LUA = &quot;&quot;&quot;\nlocal key = KEYS[1]\nlocal rate = tonumber(ARGV[1])\nlocal capacity = tonumber(ARGV[2])\nlocal now = tonumber(ARGV[3])\nlocal bucket = redis.call('HMGET', key, 'tokens', 'ts')\nlocal tokens = tonumber(bucket[1]) or capacity\nlocal ts = tonumber(bucket[2]) or now\ntokens = math.min(capacity, tokens + (now - ts) * rate)\nlocal allowed = 0\nif tokens &gt;= 1 then\n  tokens = tokens - 1\n  allowed = 1\nend\nredis.call('HMSET', key, 'tokens', tokens, 'ts', now)\nredis.call('EXPIRE', key, 60)\nreturn allowed\n&quot;&quot;&quot;\n_take = r.register_script(_TOKEN_BUCKET_LUA)\n\n\ndef acquire_token(bucket_key: str = &quot;rl:send&quot;) -&gt; bool:\n    return bool(_take(keys=[bucket_key], args=[RATE, CAPACITY, time.time()]))\n\n\ndef wait_for_token(bucket_key: str = &quot;rl:send&quot;) -&gt; None:\n    while not acquire_token(bucket_key):\n        time.sleep(0.02)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Call \u003Ccode>wait_for_token()\u003C\u002Fcode> immediately before each provider call in the worker. Because the bucket lives in Redis, \u003Cem>all\u003C\u002Fem> workers share one global rate limit — adding workers increases concurrency but not your send rate, which is exactly what you want.\u003C\u002Fp>\n\u003Ch3>Handling \u003Ccode>429\u003C\u002Fcode> from the provider\u003C\u002Fh3>\n\u003Cp>Even with client-side limiting, always honor the provider's \u003Ccode>Retry-After\u003C\u002Fcode> header. It's the authoritative signal:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\">def send_with_backoff(job: dict) -&gt; None:\n    while True:\n        resp = send_via_provider(job)\n        if resp.status_code == 429:\n            wait = int(resp.headers.get(&quot;Retry-After&quot;, &quot;1&quot;))\n            time.sleep(wait)\n            continue\n        resp.raise_for_status()\n        return\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>Concurrency: Scale Workers Horizontally\u003C\u002Fh2>\n\u003Cp>Concurrency is how you achieve throughput; rate limiting is how you cap it safely. The two work together.\u003C\u002Fp>\n\u003Cp>The right model to send millions of emails is \u003Cstrong>many small workers behind a shared rate limiter\u003C\u002Fstrong>, not one giant multithreaded process. This gives you:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Horizontal scaling\u003C\u002Fstrong> — add worker pods\u002Fcontainers to drain the queue faster (up to your rate limit).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Fault isolation\u003C\u002Fstrong> — one worker crashing doesn't take down delivery.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Simple ops\u003C\u002Fstrong> — autoscale on queue depth; scale to zero when idle.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>Sizing the worker pool\u003C\u002Fh3>\n\u003Cp>The math is straightforward:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>required_workers ≈ (target_send_rate × avg_request_latency) \u002F 1\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>If each provider call takes ~200 ms and you want to sustain 20 sends\u002Fsecond:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>workers ≈ 20 × 0.2 = 4 concurrent in-flight requests\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>So \u003Cstrong>a handful of workers\u003C\u002Fstrong> sustains a million emails a month with room to spare. You provision more not for the average but to drain bursts quickly. A common pattern: autoscale worker count based on queue depth.\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Queue depth\u003C\u002Fth>\n\u003Cth>Action\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>&lt; 1,000\u003C\u002Ftd>\n\u003Ctd>Run baseline workers (e.g. 2)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>1,000–10,000\u003C\u002Ftd>\n\u003Ctd>Scale to 8 workers\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>&gt; 10,000\u003C\u002Ftd>\n\u003Ctd>Scale to 24 workers (still capped by the shared rate limiter)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Crucially, scaling workers never lets you exceed the global rate limit, because the token bucket is shared. You drain the \u003Cem>backlog\u003C\u002Fem> faster without sending \u003Cem>per-second\u003C\u002Fem> faster than reputation allows.\u003C\u002Fp>\n\u003Ch3>Async concurrency in one process\u003C\u002Fh3>\n\u003Cp>If you prefer fewer processes, an async worker handles many in-flight requests on one thread:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\">import asyncio\nimport json\nimport aiohttp\nimport redis.asyncio as aioredis\n\nPROVIDER_URL = &quot;https:\u002F\u002Fapi.postwing.app\u002Fv1\u002Fsend&quot;\nAPI_KEY = &quot;pw_live_xxx&quot;\nCONCURRENCY = 8\n\n\nasync def send_one(session: aiohttp.ClientSession, job: dict) -&gt; None:\n    async with session.post(\n        PROVIDER_URL,\n        headers={&quot;Authorization&quot;: f&quot;Bearer {API_KEY}&quot;},\n        json=job,\n        timeout=aiohttp.ClientTimeout(total=15),\n    ) as resp:\n        resp.raise_for_status()\n\n\nasync def worker(name: int, redis_client, session) -&gt; None:\n    while True:\n        item = await redis_client.brpop(&quot;email:outbound&quot;, timeout=5)\n        if item is None:\n            continue\n        job = json.loads(item[1])\n        # wait_for_token() equivalent goes here before sending\n        try:\n            await send_one(session, job)\n        except Exception:\n            await redis_client.lpush(&quot;email:dead&quot;, json.dumps(job))\n\n\nasync def main() -&gt; None:\n    redis_client = aioredis.Redis(host=&quot;localhost&quot;, port=6379, db=0)\n    async with aiohttp.ClientSession() as session:\n        await asyncio.gather(\n            *(worker(i, redis_client, session) for i in range(CONCURRENCY))\n        )\n\n\nif __name__ == &quot;__main__&quot;:\n    asyncio.run(main())\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Async shines when calls are I\u002FO-bound (they are — you're waiting on HTTP). One async worker process with 8–16 concurrent tasks easily covers this scale.\u003C\u002Fp>\n\u003Ch2>Multiple Domains and IPs: Isolate and Protect Reputation\u003C\u002Fh2>\n\u003Cp>At a million emails a month, sender reputation becomes your most valuable — and most fragile — asset. The single most effective architectural decision is \u003Cstrong>stream isolation\u003C\u002Fstrong>: separate your mail by type onto different domains (and, at higher volume, different IPs).\u003C\u002Fp>\n\u003Ch3>Why separate streams\u003C\u002Fh3>\n\u003Cp>Different email types have wildly different engagement and risk profiles:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Transactional\u003C\u002Fstrong> (password resets, receipts, 2FA codes) — high engagement, low complaint rate, business-critical.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Notifications\u003C\u002Fstrong> (digests, alerts) — medium engagement.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Marketing \u002F lifecycle\u003C\u002Fstrong> (newsletters, re-engagement) — higher complaint and unsubscribe rates.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>If you send all three from one domain, a marketing campaign that triggers spam complaints will drag down the reputation of your password-reset emails — and now users can't log in. \u003Cstrong>Isolating streams keeps a problem in one lane from contaminating the others.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cp>A typical setup:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Stream\u003C\u002Fth>\n\u003Cth>Subdomain\u003C\u002Fth>\n\u003Cth>Why\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Transactional\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>mail.yourapp.com\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Protect critical delivery\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Notifications\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>notify.yourapp.com\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Medium-risk, separate reputation\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Marketing\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>news.yourapp.com\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>Quarantine complaint risk\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Each subdomain gets its own SPF, DKIM, and DMARC records, so reputation accrues independently.\u003C\u002Fp>\n\u003Ch3>Dedicated IPs and warm-up\u003C\u002Fh3>\n\u003Cp>Below roughly 100,000 emails a month, a \u003Cstrong>shared IP pool\u003C\u002Fstrong> (managed by your provider) is usually better — your volume is too low to build IP reputation on a dedicated IP, and a quiet dedicated IP looks suspicious to ISPs.\u003C\u002Fp>\n\u003Cp>At a million a month and growing, a \u003Cstrong>dedicated IP\u003C\u002Fstrong> becomes viable and gives you control. But a new IP has zero reputation and must be \u003Cem>warmed up\u003C\u002Fem> — sending volume must ramp gradually so ISPs learn to trust it:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Day\u003C\u002Fth>\n\u003Cth>Daily volume (example)\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>1\u003C\u002Ftd>\n\u003Ctd>50\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>2\u003C\u002Ftd>\n\u003Ctd>100\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>3\u003C\u002Ftd>\n\u003Ctd>500\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>7\u003C\u002Ftd>\n\u003Ctd>5,000\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>14\u003C\u002Ftd>\n\u003Ctd>25,000\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>30+\u003C\u002Ftd>\n\u003Ctd>Full volume\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Blasting a cold IP with 33,000 emails on day one guarantees deferrals and blocks. Most teams should let their provider manage warm-up and IP pooling rather than doing it by hand.\u003C\u002Fp>\n\u003Ch2>Retries: Fail Gracefully, Never Lose a Message\u003C\u002Fh2>\n\u003Cp>Transient failures are normal at scale — timeouts, \u003Ccode>429\u003C\u002Fcode>s, brief provider blips, ISP greylisting. The rule: \u003Cstrong>retry transient failures with exponential backoff; never retry permanent ones; never lose a message.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch3>Classify before you retry\u003C\u002Fh3>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Failure type\u003C\u002Fth>\n\u003Cth>Examples\u003C\u002Fth>\n\u003Cth>Action\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Transient\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>429\u003C\u002Fcode>, \u003Ccode>5xx\u003C\u002Fcode>, timeout, connection reset\u003C\u002Ftd>\n\u003Ctd>Retry with backoff\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Soft bounce\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Mailbox full, greylisting (\u003Ccode>4xx\u003C\u002Fcode> SMTP)\u003C\u002Ftd>\n\u003Ctd>Retry later (provider usually handles)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Permanent\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>Invalid recipient, \u003Ccode>5xx\u003C\u002Fcode> SMTP, hard bounce\u003C\u002Ftd>\n\u003Ctd>Do \u003Cstrong>not\u003C\u002Fstrong> retry; suppress address\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Bad request\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>400\u003C\u002Fcode>, malformed payload\u003C\u002Ftd>\n\u003Ctd>Do \u003Cstrong>not\u003C\u002Fstrong> retry; alert (it's a bug)\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Retrying a hard bounce is one of the fastest ways to destroy your reputation — repeatedly hammering a dead address is exactly what ISPs penalize.\u003C\u002Fp>\n\u003Ch3>Exponential backoff with jitter\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-python\">import random\nimport time\n\n\ndef retry_delays(max_retries: int = 6, base: float = 2.0, cap: float = 300.0):\n    &quot;&quot;&quot;Yield backoff delays: 2s, 4s, 8s ... capped, with jitter.&quot;&quot;&quot;\n    for attempt in range(max_retries):\n        delay = min(cap, base * (2 ** attempt))\n        # Full jitter avoids thundering-herd retries after an outage.\n        yield random.uniform(0, delay)\n\n\ndef process_with_retries(job: dict) -&gt; bool:\n    for delay in retry_delays():\n        try:\n            resp = send_via_provider(job)\n            if resp.status_code in (429,) or resp.status_code &gt;= 500:\n                time.sleep(delay)\n                continue\n            resp.raise_for_status()\n            return True\n        except requests.RequestException:\n            time.sleep(delay)\n    return False  # exhausted -&gt; send to dead-letter queue\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Jitter matters: after a provider outage recovers, you don't want every worker retrying in lockstep and re-creating the outage.\u003C\u002Fp>\n\u003Ch3>The dead-letter queue\u003C\u002Fh3>\n\u003Cp>When retries are exhausted, move the message to a DLQ rather than dropping it. The DLQ is your safety net and audit trail — you can inspect why messages failed, fix a bug, and replay them. A queue with no DLQ silently loses mail under load, which is the worst possible failure for transactional email.\u003C\u002Fp>\n\u003Ch2>Monitoring: See Problems Before Users Do\u003C\u002Fh2>\n\u003Cp>You cannot scale email infrastructure you can't observe. At a million a month, a 2% delivery drop is 20,000 failed emails — but it's invisible unless you measure it. Provider acceptance (\u003Ccode>250 OK\u003C\u002Fcode> \u002F \u003Ccode>200\u003C\u002Fcode>) tells you nothing about whether the message reached the inbox.\u003C\u002Fp>\n\u003Ch3>Metrics to track\u003C\u002Fh3>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Metric\u003C\u002Fth>\n\u003Cth>Formula\u003C\u002Fth>\n\u003Cth>Healthy target\u003C\u002Fth>\n\u003Cth>Alert threshold\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Delivery rate\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>delivered ÷ sent\u003C\u002Ftd>\n\u003Ctd>&gt; 98%\u003C\u002Ftd>\n\u003Ctd>&lt; 95%\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Hard bounce rate\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>hard bounces ÷ sent\u003C\u002Ftd>\n\u003Ctd>&lt; 0.5%\u003C\u002Ftd>\n\u003Ctd>&gt; 2%\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Complaint rate\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>complaints ÷ delivered\u003C\u002Ftd>\n\u003Ctd>&lt; 0.1%\u003C\u002Ftd>\n\u003Ctd>&gt; 0.3%\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Queue depth\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>pending messages\u003C\u002Ftd>\n\u003Ctd>near 0 baseline\u003C\u002Ftd>\n\u003Ctd>sustained growth\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Send latency\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>enqueue → delivered\u003C\u002Ftd>\n\u003Ctd>seconds–minutes\u003C\u002Ftd>\n\u003Ctd>minutes growing\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Cstrong>Retry\u002FDLQ rate\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003Ctd>retried or dead ÷ sent\u003C\u002Ftd>\n\u003Ctd>low\u003C\u002Ftd>\n\u003Ctd>spiking\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Track every one of these \u003Cstrong>per sending domain and per email type\u003C\u002Fstrong>. An aggregate 98% delivery rate can hide a \u003Ccode>password_reset\u003C\u002Fcode> stream at 80% because of one broken DKIM record. Segmentation is what makes monitoring actionable.\u003C\u002Fp>\n\u003Ch3>Capture events with webhooks\u003C\u002Fh3>\n\u003Cp>Delivery, bounce, and complaint events arrive asynchronously via webhooks. Acknowledge fast, process async, and feed both metrics and the suppression list:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-python\">from flask import Flask, request, jsonify\n\napp = Flask(__name__)\n\n\n@app.post(&quot;\u002Fwebhooks\u002Femail&quot;)\ndef email_events():\n    event = request.get_json(force=True)\n    event_type = event.get(&quot;type&quot;)\n    address = event.get(&quot;recipient&quot;)\n\n    if event_type in (&quot;bounced&quot;, &quot;complained&quot;):\n        add_to_suppression_list(address)  # never email this address again\n\n    record_metric(event_type, stream=event.get(&quot;stream&quot;))\n    return jsonify({&quot;ok&quot;: True}), 200\n\n\ndef add_to_suppression_list(address: str) -&gt; None:\n    ...  # write to your suppression store\n\n\ndef record_metric(event_type: str, stream: str) -&gt; None:\n    ...  # increment counters in your metrics backend\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Wire threshold alerts (delivery-rate cliff, complaint spike) \u003Cstrong>and\u003C\u002Fstrong> a zero-volume alarm so a silent pipeline failure doesn't masquerade as health.\u003C\u002Fp>\n\u003Ch2>Cost at Scale: What a Million Emails Really Costs\u003C\u002Fh2>\n\u003Cp>Cost has two components: \u003Cstrong>the email provider\u003C\u002Fstrong> and \u003Cstrong>your own infrastructure.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch3>Provider cost\u003C\u002Fh3>\n\u003Cp>Transactional providers price per thousand emails or via monthly tiers. At a million a month, expect a meaningful but not enormous bill:\u003C\u002Fp>\n\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Pricing model\u003C\u002Fth>\n\u003Cth>Typical range (1M\u002Fmonth)\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\n\u003Ctr>\n\u003Ctd>Per-email metered\u003C\u002Ftd>\n\u003Ctd>~$0.10–$1.00 per 1,000 → \u003Cstrong>$100–$1,000\u002Fmo\u003C\u002Fstrong>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Flat monthly tier\u003C\u002Ftd>\n\u003Ctd>Often \u003Cstrong>$80–$400\u002Fmo\u003C\u002Fstrong> for ~1M\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>Self-hosted SMTP (own MTA)\u003C\u002Ftd>\n\u003Ctd>\"Cheaper\" per-email, high engineering + deliverability cost\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\n\u003C\u002Ftable>\n\u003Cp>Self-hosting your own mail transfer agent (Postfix\u002FHaraka on bare metal) looks cheapest on paper, but you then own IP warm-up, blocklist remediation, feedback loops, and on-call deliverability — months of engineering that adds no product value. For almost every SaaS, \u003Cstrong>a managed provider is cheaper once you price in engineering time.\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch3>Your infrastructure cost\u003C\u002Fh3>\n\u003Cp>The queue + workers footprint for this scale is small:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>Redis\u003C\u002Fstrong>: a small managed instance (~$15–$50\u002Fmo).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Workers\u003C\u002Fstrong>: a couple of small containers, autoscaling on burst (~$20–$80\u002Fmo).\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Webhook consumer + metrics\u003C\u002Fstrong>: minimal.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Total self-run infrastructure for the \u003Cem>plumbing\u003C\u002Fem> is often under $150\u002Fmonth — the queue and workers are cheap; the value (and most of the cost) is in deliverability and reputation, which is exactly what a good provider handles for you.\u003C\u002Fp>\n\u003Ch3>Cost optimization levers\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>\u003Cstrong>Suppress aggressively\u003C\u002Fstrong> — every email to a dead address is wasted spend \u003Cem>and\u003C\u002Fem> reputation damage.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Validate at capture\u003C\u002Fstrong> — reject malformed addresses before they enter the queue.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Batch where the provider supports it\u003C\u002Fstrong> — fewer API calls, lower overhead.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>Right-size workers\u003C\u002Fstrong> — autoscale down when the queue is empty; don't pay for idle concurrency.\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>Common Mistakes When You Send Millions of Emails\u003C\u002Fh2>\n\u003Ch3>1. Sending synchronously from request handlers\u003C\u002Fh3>\n\u003Cp>Calling the email API inside a user request couples your app's responsiveness to the provider's. A blip becomes a user-facing timeout, and a crash loses the message. Always enqueue, never send inline.\u003C\u002Fp>\n\u003Ch3>2. No global rate limiter across workers\u003C\u002Fh3>\n\u003Cp>Adding workers without a \u003Cem>shared\u003C\u002Fem> rate limiter multiplies your send rate and triggers provider \u003Ccode>429\u003C\u002Fcode>s and ISP throttling. The rate limit must be global, not per-worker.\u003C\u002Fp>\n\u003Ch3>3. Treating \"accepted\" as \"delivered\"\u003C\u002Fh3>\n\u003Cp>A \u003Ccode>200\u003C\u002Fcode>\u002F\u003Ccode>250 OK\u003C\u002Fcode> means the provider queued the message, not that it reached the inbox. Without webhook-based delivery tracking you're blind to the half of the lifecycle that actually matters.\u003C\u002Fp>\n\u003Ch3>4. Mixing all email types on one domain\u003C\u002Fh3>\n\u003Cp>One spammy marketing run can tank the reputation that your password-reset emails depend on. Isolate transactional, notification, and marketing streams onto separate subdomains.\u003C\u002Fp>\n\u003Ch3>5. Retrying permanent failures\u003C\u002Fh3>\n\u003Cp>Re-sending to hard-bounced or complained addresses is the fastest route to a blocklist. Classify failures and suppress permanent ones immediately.\u003C\u002Fp>\n\u003Ch3>6. No dead-letter queue\u003C\u002Fh3>\n\u003Cp>A pipeline that drops messages when retries are exhausted loses transactional mail silently under exactly the load conditions where it matters most. Always capture exhausted jobs in a DLQ.\u003C\u002Fp>\n\u003Ch3>7. Cold-IP blasting \u002F skipping warm-up\u003C\u002Fh3>\n\u003Cp>Pointing full volume at a brand-new dedicated IP guarantees deferrals and blocks. Warm up gradually, or let your provider manage IP pooling.\u003C\u002Fp>\n\u003Ch3>8. Aggregating metrics into one number\u003C\u002Fh3>\n\u003Cp>A single global delivery rate hides per-stream failures. Always segment monitoring by domain and email type.\u003C\u002Fp>\n\u003Ch2>Frequently Asked Questions\u003C\u002Fh2>\n\u003Ch3>How do I send millions of emails per month without getting blocked?\u003C\u002Fh3>\n\u003Cp>Decouple acceptance from delivery with a durable queue, throttle sends with a shared rate limiter that respects both your provider and ISP limits, isolate email types onto separate authenticated subdomains, suppress bounces and complaints automatically, and warm up any new IP gradually. Getting blocked at this scale is almost always a reputation problem caused by sending too fast from a cold sender or repeatedly mailing bad addresses — not a raw-volume problem. A million a month is a small workload for properly designed scalable email infrastructure.\u003C\u002Fp>\n\u003Ch3>What is the best architecture for scalable email infrastructure?\u003C\u002Fh3>\n\u003Cp>The proven pattern is: application → durable queue → rate-limited worker pool → email provider → async webhook event consumer → metrics, suppression, and alerting. The queue absorbs bursts and guarantees durability; the worker pool provides horizontal concurrency; a shared rate limiter caps the send rate to what your reputation can sustain; and webhooks give you delivery visibility. This decouples your app's responsiveness from delivery and lets each layer scale independently.\u003C\u002Fp>\n\u003Ch3>How many workers do I need to send 1 million emails a month?\u003C\u002Fh3>\n\u003Cp>Far fewer than people expect. One million a month averages under one email per second, so even at a peak of a few sends per second with ~200 ms provider latency, you need only a handful of concurrent in-flight requests — roughly 4–8 workers, or a single async worker process. You provision extra workers not for the average rate but to drain bursts quickly; a shared rate limiter keeps the per-second send rate safe no matter how many workers you run.\u003C\u002Fp>\n\u003Ch3>Do I need a dedicated IP to send millions of emails?\u003C\u002Fh3>\n\u003Cp>Not at one million a month. Below roughly 100,000 a month a shared IP pool managed by your provider is usually better, because your volume is too low to build reputation on a dedicated IP — and a quiet dedicated IP looks suspicious to ISPs. Around a million a month and climbing, a dedicated IP becomes viable and gives you more control, but it must be warmed up gradually. For most teams, letting the provider manage IP pooling and warm-up is the better trade-off.\u003C\u002Fp>\n\u003Ch3>How should I handle retries when sending email at scale?\u003C\u002Fh3>\n\u003Cp>Classify failures first. Retry transient errors (\u003Ccode>429\u003C\u002Fcode>, \u003Ccode>5xx\u003C\u002Fcode>, timeouts) with exponential backoff plus jitter; never retry permanent failures (invalid recipients, hard bounces) because re-mailing dead addresses destroys your reputation; and when retries are exhausted, move the message to a dead-letter queue rather than dropping it. Jitter prevents every worker from retrying in lockstep after an outage and re-creating it.\u003C\u002Fp>\n\u003Ch3>What does it cost to send a million emails a month?\u003C\u002Fh3>\n\u003Cp>Provider cost typically lands between roughly $100 and $1,000 per month depending on pricing model, with many flat tiers around $80–$400. Your own plumbing — a small Redis instance plus a couple of autoscaling worker containers — is often under $150 a month. Self-hosting your own MTA looks cheaper per email but adds months of deliverability engineering and ongoing on-call, so a managed provider is usually cheaper once you price in engineering time.\u003C\u002Fp>\n\u003Ch3>How do I monitor email delivery at scale?\u003C\u002Fh3>\n\u003Cp>Ingest delivery, bounce, and complaint webhooks from your provider, then compute delivery rate, hard bounce rate, complaint rate, queue depth, send latency, and DLQ rate — segmented per sending domain and per email type. Set threshold alerts (a delivery-rate cliff, a complaint spike) and a zero-volume alarm so a silent pipeline failure doesn't pass for health. Aggregate-only metrics hide per-stream failures, which are the ones that actually break user flows.\u003C\u002Fp>\n\u003Ch3>Why use a queue instead of sending email directly from my app?\u003C\u002Fh3>\n\u003Cp>A queue decouples accepting an email from delivering it. Your app enqueues in microseconds and returns, so provider slowness never becomes user-facing slowness; messages persist through crashes and deploys instead of vanishing from memory; bursts are absorbed and drained at a safe rate; and your web tier and delivery tier scale independently. Sending directly from request handlers couples your application's reliability to your email provider's — the opposite of what scalable email infrastructure should do.\u003C\u002Fp>\n\u003Ch2>Conclusion\u003C\u002Fh2>\n\u003Cp>Scaling to \u003Cstrong>send millions of emails\u003C\u002Fstrong> a month is an exercise in decoupling and discipline, not raw horsepower. A million a month is a modest rate — under one email per second on average — but real traffic is bursty and sender reputation is fragile, so the architecture has to absorb spikes without losing messages or overrunning ISPs.\u003C\u002Fp>\n\u003Cp>The blueprint is consistent: a durable queue to buffer and protect against loss, a shared rate limiter that respects both provider and ISP limits, a horizontally scaled worker pool for concurrency, stream isolation across authenticated domains to protect reputation, exponential-backoff retries with a dead-letter queue so nothing is silently dropped, and per-stream monitoring with alerting so you see trouble before your users do. Each piece is implementable incrementally — start with the queue, then add rate limiting, then monitoring.\u003C\u002Fp>\n\u003Cp>Build it this way and a million emails a month is routine, and the same design carries you to ten million with little more than more workers and a dedicated IP. The teams that struggle are the ones still sending synchronously from request handlers and discovering reputation problems only after delivery has already collapsed.\u003C\u002Fp>\n\u003Ch2>Send and Scale Transactional Email with Postwing\u003C\u002Fh2>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fpostwing.app\">Postwing\u003C\u002Fa> is a transactional email platform built for developers and SaaS companies that need \u003Cstrong>scalable email infrastructure\u003C\u002Fstrong> without building the deliverability stack themselves. You get a fast send API designed to sit behind your queue, generous rate limits, automatic suppression of bounces and complaints, managed IP pooling and warm-up, and webhook events for every lifecycle stage — so the queueing, rate limiting, retries, and monitoring patterns in this guide plug straight in.\u003C\u002Fp>\n\u003Cp>SPF, DKIM, and DMARC setup, multiple sending domains for stream isolation, and per-domain delivery dashboards come built in, so you can isolate transactional from marketing reputation without operating your own MTAs. And because Postwing accepts \u003Cstrong>USDC payments on Base\u003C\u002Fstrong>, international founders and crypto-native teams can scale up without card requirements or traditional payment friction.\u003C\u002Fp>\n\u003Cp>Stop wiring deliverability together by hand. \u003Cstrong>\u003Ca href=\"https:\u002F\u002Fpostwing.app\">Start sending millions of emails with Postwing →\u003C\u002Fa>\u003C\u002Fstrong>\u003C\u002Fp>","0ff7c856-51c6-4430-9a20-91eb18ae4981","2026-06-24T18:28:18.074703+03:00","2026-07-15T23:56:49.381176+03:00","postwing","How to Scale to 1 Million Emails per Month","Learn how to send millions of emails a month: queueing, rate limits, concurrency, multiple domains, retries, and monitoring for scalable email infrastructure.","https:\u002F\u002Fapi.postwing.app\u002Fmedia\u002Fblog\u002Frecord_0ff7c856-51c6-4430-9a20-91eb18ae4981\u002Fscale-to-one-million-emails.png",true,"2026-08-05T09:00:00+03:00",[16,17],"send millions of emails","scalable email infrastructure"]