Can you handle our volume?
The real numbers behind a five million contact import and a half million person send, and the limits we put on both.
Every serious sender asks us the same thing on the first call. Some version of: you're new, can you actually handle our volume?
It is a fair question, and we get why it comes loaded. Nitrosend is a young company. A team evaluating us is often moving away from a platform they have outgrown or stopped trusting, and the last thing they want is to find the ceiling the hard way, mid-campaign, with a launch on the line.
So rather than answer with a number on a slide, here is the machine. What it costs to move five million contacts into Nitrosend, what happens when you send a campaign to half a million people, and the limits we put on both. The limits are the part most platforms won't show you. They are also the part that matters most, so they get their own section.
Why the question comes up now
Bigger senders started showing up when we shipped the multi-brand architecture, so it is worth explaining what that actually is, because it is the reason a serious team looks twice.
In Nitrosend an account can own many brands, and a brand is not a label. It is a sending identity. Each brand owns its own sending domain, its own DKIM signing keys, and its own reputation counters. Bounce rate and complaint rate are tracked per domain, and every send is gated on the health of the specific domain it goes out on. If one brand's domain starts bouncing, that domain gets throttled or held. The other brands keep sending on their own clean domains, unaffected, because they are signed by different keys on different domains with their own separate history.
For a holding company running twelve storefronts, an agency running mail for forty clients, or a platform sending on behalf of its own customers, that isolation is the whole game. One brand's bad week does not become everyone's bad week.
I want to be precise about the boundary of that claim, because overselling it is how people get burned. Isolation is at the domain, the DKIM keys, and the per-domain health gate. It is not total. Brands inside one account still share an account-level suppression list, and they share the account's standing with whichever provider carries the send. Aggregate abusive traffic can still drag on the account as a whole, which is exactly why warmup limits and suppression stay switched on no matter how good your domains look. Separate domains buy you a lot. They do not buy you immunity, and anyone who tells you otherwise is selling.
Moving five million contacts
Start with the import, because it is the first thing a large sender does and the first place a platform tends to fall over.
A five million row CSV is about 442 MB. The naive version of this, the one a lot of tools still ship, streams that file up through the application server, parses the whole thing in memory, and saves contacts one row at a time through the ORM. Every one of those choices breaks at scale. So we made the opposite choice at every step.
contacts-5m.csv (442 MB)
| direct signed PUT to object storage, bytes never touch the API
v
row index single pass, byte offsets recorded per batch boundary
|
v
1,000 batches 5,000 rows each, 4 wide (8 under operator override)
| each batch seeks its own byte range, no re-parse
v
bulk insert insert_all / upsert_all, ~6 statements per 5,000 rows
| flow triggers and per-address verification suppressed, reconciled after
v
contacts live memberships, channels, field catalog written in bulk The bytes never touch our API. The client asks for a signed URL and uploads the file straight to object storage. In our own runs, the 442 MB five million row file goes up in a single signed PUT in about two and a half minutes. A 500,000 row file, around 46 MB, goes up in roughly sixteen seconds. That is the network, not us. Our application never sees the payload, so there is no request timeout to hit and no memory to blow.
We read the file once. A single pass records byte offsets at each batch boundary and nothing more. Then the work fans out into a thousand batches of five thousand rows, and each batch seeks directly to its own byte range in storage. No batch ever re-reads the rows before it. That is the difference between a job that scales linearly and one that quietly goes quadratic on you at three million rows.
We write in bulk, not row by row. Each batch is a handful of set-based statements, insert_all and upsert_all, roughly six per five thousand rows: contacts inserted, contacts updated, channels created, channels subscribed, list memberships written, list counts rolled up. There are no per-row ActiveRecord saves on the hot path. Batches run four at a time by default, eight under an operator override for the largest jobs.
We turn off the fan-out during the bulk load. A single contact added through the normal path is more than a row. It evaluates automation triggers and can enqueue an address verification job. That is correct behaviour for one contact and ruinous behaviour for five million. During a bulk import we suppress the trigger fan-out and the per-address verification entirely, then reconcile afterward. The import is a data-move, not five million individual signup events, and treating it that way is most of the speed.
We measured all of this by pushing real files through the same public endpoints a customer hits, with real writes landing in the production database, not a private benchmark path that skips the parts that are slow. If the drill cheated, the number would be a lie, and a lie about your infrastructure is the most expensive kind.
The guardrails on import
Size alone decides how an import is handled, and the tiers are deliberate:
| Import size | What happens |
|---|---|
| Up to 20,000 | Processed automatically, no gate |
| 20,000 to 250,000 | Processed, but sends are held for a quick review first |
| Over 250,000 | You talk to us before it runs |
The middle tier exists because a sudden quarter-million contact import followed by an immediate campaign is either a great launch or a list you should not be mailing, and from the outside those look identical for the first few minutes. Holding the send, not the import, lets the data land while a human confirms the sending is legitimate. The top tier is not a capacity limit. Five million imports fine. It is a conversation, because a list that size almost always has a warmup and deliverability plan attached, and we would rather have that conversation before the send than during the incident.
Sending to five hundred thousand
Here is where the honest answer diverges from the one you might expect. The question is usually phrased as "can you send half a million emails fast," and the true answer is: it depends entirely on who is asking, and a brand-new account cannot do it at all on day one. That is not a weakness. It is the single most important thing we do for your deliverability.
Two things gate every large send. The first is warmup, a daily cap tied to your plan and how long your sending domain has been verified. A cold domain that suddenly emits half a million messages is the clearest spam signal there is, so we ramp you:
| Domain age | Free | Paid | Trusted |
|---|---|---|---|
| 0 to 3 days | 50/day | 1,000/day | 5,000/day |
| 4 to 7 days | 100/day | 3,000/day | 15,000/day |
| 8 to 14 days | 250/day | 10,000/day | 50,000/day |
| 15 to 21 days | 500/day | 30,000/day | Unlimited |
| 22 to 35 days | 1,000/day | Unlimited | Unlimited |
| 36+ days | Unlimited | Unlimited | Unlimited |
The second gate is pacing. Even once you are cleared to send the volume, we do not fire it all at the provider in one burst. Recipients are enqueued in blocks with a pause between blocks, and the size of the block and the length of the pause are set by how much trust your account has earned. This is the ladder, and it is the exact table the system reads at send time:
| Tier | Who is on it | Block | Pause | Roughly |
|---|---|---|---|---|
| Bounded | New and unverified accounts | 500 | 10 min | 3,000/hr |
| Paid starter | Paying, still earning trust | 250 | 2 min | 7,500/hr |
| Checkpointed | Graduated reputation | 500 | 90 sec | 20,000/hr |
| Stream | Graduated, granted trusted-operator | 25,000 | none | Provider-bound |
| System | Nitrosend's own platform traffic | 25,000 | none | Provider-bound |
Read the top two rows as a feature, not a restriction. A brand-new account sending on a fresh domain is metered to a few thousand an hour on purpose, because at that stage every send is a bet on an unproven reputation, and pacing is how we keep one aggressive first campaign from torching a domain you will want to use for years.
Read the bottom two rows as the answer to the original question. Once an account has graduated its reputation and earned trusted-operator status, the enqueue pauses go to zero and blocks jump to twenty-five thousand. At that point Nitrosend stops being the bottleneck. The only thing pacing the send is the provider's own throughput, and we deliberately run at 95% of whatever rate the provider reports, so we stay under its ceiling instead of tripping it. For a warmed, trusted domain, half a million messages clears in about an hour and a half at the current provider ceiling, bound by the wire and not by us. We measured that instead of modelling it, and the numbers are in the next section. For a domain that verified this morning, the honest answer is that it should not be sending half a million anything, and the machine will not let it.
We route across more than one sending provider, and the pacing math is the same regardless of which one carries a given send. The tier decides your rate. The provider decides its own ceiling. We stay under both.
The send, measured
Those tables are policy. Here is what the machine does once policy gets out of the way, measured on production, not on a laptop and not in a staging environment with the hard parts turned off.
| What | Volume | Rate | Time |
|---|---|---|---|
| Trusted sender, provider-capped | 500,000 | ~100/sec, flat at the cap | ~90 minutes |
| Nitrosend pipeline, provider cap removed | 500,000 | ~1,380/sec | ~6 minutes |
The first row is what a trusted account gets today: the whole campaign path end to end, outbox, billing, pacing, dispatch, out through the provider at the provider's rate. The steady-state rate sits pinned at the cap from first message to last, zero failures. That is the practical number for a big sender: your entire half-million campaign is out the door in about an hour and a half, today, on a trusted account. And at that rate the platform is not the limiter. The provider is, and we sit just under its ceiling and hold there, flat, until the send is done.
The second row is the same pipeline with the gate taken off, so it answers the question the first row cannot: what is Nitrosend actually capable of when the provider cap is out of the way. Half a million messages at 1,380 a second, about six minutes end to end, zero failures. That is roughly fourteen times the current enterprise rate. The cap you send at today is the provider's, not ours, and as provider quotas rise with volume history, the headroom is already built and already measured.
That gap is the point of publishing both rows. Plenty of platforms can hit a 100 a second ceiling. The question a serious sender should ask is what happens the day the ceiling moves. Provider quotas move when a sender proves volume, and a platform either has headroom waiting or it becomes the next bottleneck. Here, the answer is: nothing. It was never our ceiling.
What a send is actually made of
Throughput numbers earn nothing unless you can see the machinery behind them, so here is what every one of those half million messages goes through, and where the time goes.
The outbox comes first. When a campaign fires, recipients are resolved out of their lists and segments into a durable outbox, written in 5,000-row set-based batches. That is the honest database work of a send and we do it up front, before the first byte touches the provider, so the fire phase reads from a prepared queue instead of paying segmentation costs per message.
Sends are reserved in blocks of fifty. Workers do not take one message at a time. The rate limiter hands out reservations of fifty sends, which is also exactly the ceiling of a single call to Amazon SES's bulk API. Fifty recipients per call means half a million messages is exactly 10,000 HTTPS calls to SES. At the trusted account rate the machine paces those at two calls a second. Ungated, it sustains about twenty-eight.
Provider calls overlap, never queue. The lane keeps up to twenty bulk calls in flight at once, so the round-trip time of any single SES call is hidden behind the nineteen others. At the capped rate, provider latency is invisible because pacing dominates. At full throughput, it still does not serialize the send. A slow call slows one lane, not the campaign.
Safety is checked during the send, not just before it. Campaign still active, send token still current, account not silenced, sending domain still healthy. Those checks run continuously through the send window, batched and cached so they cost the hot path almost nothing, but never skipped, because a send you cannot stop mid-flight is a liability dressed up as a feature.
Bookkeeping is set-based or it does not ship. Each SES call marks its fifty messages dispatched with a single UPDATE. Accepted messages update in bulk. Billing locks per chunk, not per recipient. The discipline shows up in the profile: firing 500,000 messages executed roughly 15,000 SQL statements in total, one statement per 34 messages. The naive version of this pipeline, a few statements per message, would be around a million and a half. That gap is the entire difference between a database that shrugs at a half-million send and one that becomes the incident.
How we measured it
The numbers come from an instrumented drill harness that drives the same send lane production uses. Same transmitter, same dispatcher, same pacing code, no benchmark-only fast path. Every run emits a machine-readable evidence file: phase timings, time to first acceptance, per-second acceptance buckets, and a full SQL profile of the fire window, which is how we know the statement counts above instead of guessing at them.
The provider-rate row ran as a real production campaign through the real SES endpoint, dispatched to SES's mailbox simulator addresses, which exercise the entire provider path, authentication, API and acceptance included, without landing in anyone's inbox. The per-second buckets from that run are almost boring to read: the first message is accepted about a second after fire, then the buckets sit at the cap, flat, until the last message clears. Boring is the goal. A flat line at the ceiling means pacing is doing its job and nothing upstream is starving the lane.
The ungated row ran the same code against the same production database with only the SES call stubbed out, in an isolated one-off container so live customer sends were untouched. That isolates our half of the equation: everything Nitrosend does per message, measured with the provider out of the picture. 1,380 a second is what the pipeline itself does.
What happens when it breaks
Peak throughput is the easy number. Serious senders care more about the failure modes, because that is where a platform actually lets you down. Three things we built for, all of them from having been burned:
Billing locks per chunk, not per recipient. A half million person send bills as a small number of chunk events, one usage record and one counter lock per chunk. If we took a lock per recipient, the billing system would be the bottleneck long before the mail server was. Sent messages carry their chunk's billing id so the reconciler never double-charges them.
Stuck messages fail loud, not silent. A message that gets reserved for sending but never reaches the provider inside fifteen minutes is swept up and marked failed, visibly, with a reason attached. The failure you can see is the one you can fix. A send that silently evaporates is the one that costs you a customer, so we made the stuck case loud by design, the same principle we wrote about in how we ship the send pipeline.
Pausing a send actually stops it. When you pause or cancel a campaign, the send token it was running under is invalidated, which makes every already-queued job for that send stale and inert. There is no half-second window where a cancelled campaign keeps dribbling out because jobs were already in flight. Stop means stop.
What these numbers do not promise
An honest benchmark comes with its own fine print, so here is ours.
These are the costs of the machine, not a guarantee about your inbox placement. Pacing and warmup lower the odds of a reputation problem. They do not remove it, and nothing on a slide can, because the receiving mailbox providers get the final vote and they do not publish their rules. If your list is bad, the best pipeline in the world just delivers your complaints faster.
The multi-brand isolation is real but bounded, as covered above. Separate domains and keys, shared account standing. We would rather you know exactly where that line sits than discover it later.
And we ran these drills on the real production shape on purpose, which is a deliberately unglamorous stack: one primary database, a Postgres-backed job queue, ARM boxes. The point of the numbers is not that we own a data centre. It is that a modest, honest stack handles this volume when the pipeline is built correctly, and that most of what breaks at scale is architecture, not hardware.
Common questions
How many emails can I send from a new domain?
On a fresh domain, sending is capped by a warmup ramp tied to your plan. A free domain starts at 50 a day, a paid domain at 1,000, and a trusted domain at 5,000, climbing to unlimited over the first few weeks as the domain proves itself. The cap protects the reputation you are trying to build.
How long does it take to import five million contacts?
The 442 MB file uploads straight to object storage in about two and a half minutes, then the rows are written in bulk across parallel batches rather than one at a time. We run these drills through the same public endpoints a customer uses, with real writes to the production database.
How fast can I send a campaign to 500,000 people?
It depends on how much trust your account has earned. A warmed, trusted domain clears an entire half-million campaign in about an hour and a half at the provider rate, and with the provider cap out of the way we have measured the pipeline moving the same half million in about six minutes, roughly fourteen times the current enterprise rate. A brand-new domain is paced to a few thousand an hour on purpose, because a cold domain sending that volume is the clearest spam signal there is.
Can one account send for multiple brands?
Yes. Each brand gets its own sending domain, its own signing keys, and its own reputation tracking, so one brand's bad week does not drag down the others. Brands in an account still share an account-level suppression list and the account's overall standing with the provider.
What is email domain warmup?
Warmup is the practice of ramping a new sending domain's daily volume slowly so mailbox providers learn to trust it. Nitrosend enforces it automatically with a per-plan daily cap based on how long your domain has been verified.
Close
The real answer to "can you handle our volume" is not a single throughput number, because the number that matters is different for a domain that verified today and one that has been warm for a year. What we can show you is the whole machine and every limit on it, measured through the same doors you would use, with nothing hidden behind a private fast path.
A new company earns a serious sender by showing the ceiling and the guardrails, not by promising there aren't any. If you are moving real volume and want to walk through where your specific sending would sit on these tables, that is exactly the conversation we like having.
Moving serious volume? Tell us what you send and we'll map it to the tiers above. Start free, or reach out and we'll plan the migration with you.