mailsac

Email Load Testing

High-volume email testing

Email load testing: receive thousands of test emails and check every link

A load test that creates accounts also creates email. Every virtual user needs its confirmation link or one-time code back before it can finish, so the mail has to arrive, be stored, and be readable by the load generator within seconds. Mailsac receives that mail on a private test domain, hands each message to one webhook, and lets you recover anything missed over the REST API. This page covers what a standard plan handles, when a run needs a high-throughput evaluation, and how to size it.

Reviewed September 2026 against the current pricing page, documentation and evaluation limits.

Already on a paid plan? Size your run or set up the receiver. For volume tests that only need arrivals counted, see count-only load testing.

At a glance

What it is

Standard Any paid plan with a private domain: every address receives, mail is stored and read over the API

Evaluation A two-week burst allowance on a verified private domain, set up by Mailsac on request

Count-only The Load Testing feature on Business and up: arrivals are counted, bodies are discarded

Burst rate

Standard Hundreds a minute is normal use; thousands a minute from one sender is slowed, then refused until it retries

Evaluation 50 incoming messages a second, up to 6,000 in a two-minute burst, then a one-minute cooldown

Count-only Built for high rates; nothing to read afterwards

Extract a link or code from each email

Standard Yes: webhook, WebSocket or REST

Evaluation Yes: webhook, with REST to reconcile

Count-only No

Stored messages

Standard 5,000 on Business, 10,000 on Business+, 12,000 or more on Enterprise

Evaluation 12,000 retained; 100,000 deliveries over the two weeks

Count-only None stored

How to get it

Standard Self-serve; Business is $89 a month

Evaluation Email support@team.mailsac.com with your domain and receiver

Count-only Dashboard, on Business and up

Which option fits your test?

Three questions decide it: how many messages arrive per second at the peak, whether you need to read each one, and how long the run lasts. Most functional suites fit a standard plan. A burst of thousands of confirmation emails in a couple of minutes needs an evaluation. A test of your own sending pipeline, where nobody reads the mail, is what the count-only feature is for.

A standard paid plan

  • Runs that spread a few thousand emails over ten minutes or more, or peak in the hundreds a minute. Parallel CI suites, soak tests and nightly regression are typical.
  • Any address on your private domain receives mail with nothing to create first. Give each virtual user its own address and read it by webhook, WebSocket or REST.
  • All inbound mail is throttled to keep the service up for everyone. Paid private domains have a higher threshold than public inboxes; the throttling guide says most paid customers never notice it, except when sending thousands of messages a minute.
  • Business: $89 a month, 500,000 Ops, 5,000 stored messages, 5 domains. Business+: $199, 2 million Ops, 10,000 stored. Enterprise: from $7,500 a year.

A high-throughput evaluation

  • For bursts like 4,000 sign-ups in two minutes, where every confirmation URL must come back and be followed. That is 30 to 50 messages a second from one sending service, which a standard domain will slow down.
  • Fixed allowance for two weeks: 50 incoming messages a second, up to 6,000 per two-minute burst followed by a one-minute cooldown, 100,000 stored deliveries in total, 12,000 retained messages, 100 KiB per message.
  • Webhooks plus REST text and raw retrieval. WebSocket, Slack and forwarding are not part of the evaluation. Webhook delivery is best effort, so the receiver pattern below reconciles over REST.
  • Needs a new dedicated account and a verified private custom domain. Mailsac staff start it on request and supervise the first representative run. It is a test, not a service level agreement; ongoing burst capacity is arranged on Enterprise before it ends.

Count-only load testing

  • For measuring your own sending pipeline: how fast your provider, queue and templates push mail out, with no reader on the other end.
  • Create a load test in the dashboard and Mailsac issues a temporary domain under loadtester.msdc.co. It is live five minutes later and accepts mail to any address for eight hours; the dashboard shows the count as messages arrive.
  • Messages are counted, not stored, so there is nothing to read and no link to follow. Each arrival is one Op.
  • Included on Business, Business+ and Enterprise. The walkthrough with a sample sender script and the Load Test documentation cover the details.

How a load test with email works

The load generator never talks to Mailsac during the run. It talks to a small receiver you host, which Mailsac feeds by webhook. That keeps the hot path to one HTTP request per email and leaves the REST API for setup, reconciliation and cleanup.

  1. Point a private domain at Mailsac. Use a subdomain you own (one TXT record to verify, two MX records to receive) or a zero-setup yourteam.msdc.co subdomain that needs no DNS. Either way, every address on the domain is an inbox the moment mail arrives at it. See custom domains.
  2. Turn on the catch-all and give it one webhook. On the domain’s Forwarding tab, enable the catch-all (*@yourdomain) and set its webhook to your receiver’s URL. One setting covers every address the run will ever generate. See catch-all forwarding and webhooks.
  3. Generate a unique address per virtual user. In JMeter, user-${__threadNum}-${__time()}@loadtest.yourdomain.com; in k6, user-${__VU}-${__ITER}@loadtest.yourdomain.com. Nothing is reserved or created up front; no two users share an inbox.
  4. The virtual user signs up. Your application sends the confirmation through its normal provider. Mailsac receives it like any mail server, stores it, and POSTs the message as JSON (subject, recipients, plain text, raw source) to the webhook.
  5. The receiver extracts the link or code and keeps it keyed by recipient address. The virtual user asks the receiver for its URL, waits briefly if it is not there yet, and follows it.
  6. After the burst, reconcile. List the domain over REST, compare message IDs with what the webhook delivered, fetch the text of anything missing, and follow those links too. This is what makes the run complete rather than approximate.
  7. Clean up. One call to POST /api/domains/{domain}/delete-all-domain-mail empties the domain, or let retention recycle the oldest messages. Deleting is what keeps repeated runs inside the stored-message limit.

What the run proves: your application produced and sent every confirmation, each one arrived with a working link, and the whole sign-up path holds at the tested rate. It does not test inbox placement at Gmail or Outlook, and Mailsac does not send mail.

Private domain, one webhook receiver, REST reconciliation

The receiver is about fifty lines and has two jobs: accept each webhook quickly and answer the load generator. The reconcile step runs once the burst is over.

Why this shape

One webhook, not one per inbox. Polling GET /api/addresses/{email}/messages from 4,000 virtual users every two seconds is thousands of requests a second and 3 to 6 Ops per email. The catch-all webhook is one POST per message and about 2 Ops.

Respond first, work second. Return a 2xx as soon as the body is read, then parse. Mailsac does not follow redirects, treats a slow receiver as a failure, and does not queue retries, so a receiver that blocks on a database write will lose messages it could have kept.

Idempotent by _id. Keep the set of message IDs you have seen. It makes duplicate deliveries harmless and is what the reconcile step compares against.

Reconcile over REST. GET /api/domains/{domain}/messages lists every message on the domain, newest first, with limit and until for paging. The until boundary is inclusive, so page in small steps and dedupe by ID. GET /api/text/{inbox}/{id} returns the plain text; every message object also carries a links array of URLs found in the text and HTML.

Match, don’t guess. Extract the confirmation URL by a pattern you control (a path such as /confirm, or a regex for a six-digit code). A message’s to[0].address is the address the virtual user used, even when the domain stores it under the catch-all.

JMeter and k6. Both only need three samplers per user: sign up with the generated address, poll the receiver’s /url?to= endpoint in a short loop until it returns 200, then request the URL it returned. In JMeter, put the address in a User Defined Variable so all three samplers see the same value; ${__threadNum} plus ${__time()} (or ${__UUID()}) keeps it unique. In k6, __VU and __ITER do the same. Neither tool needs a Mailsac API key: only the receiver holds one, for the reconcile step.

receiver.ts · Node.js 18 or later · no dependencies · standalone example, not a copy of repository code

import { createServer } from 'node:http';

const DOMAIN = 'loadtest.yourdomain.com';
const headers = { 'Mailsac-Key': process.env.MAILSAC_API_KEY! }; // only the reconcile step needs it
const seen = new Set<string>();          // message _id: makes duplicate deliveries harmless
const urls = new Map<string, string>();  // recipient address -> confirmation URL

function extract(text?: string, links?: string[]) {
  return links?.find((l) => l.includes('/confirm'))
    ?? text?.match(/https?:\/\/\S+\/confirm\S*/)?.[0];
}

createServer(async (req, res) => {
  if (req.method === 'POST' && req.url === '/mailsac') {          // the catch-all webhook
    let body = '';
    for await (const chunk of req) body += chunk;
    res.writeHead(204).end();                                       // answer first, then work
    const m = JSON.parse(body);
    if (seen.has(m._id)) return;
    seen.add(m._id);
    const to = (m.to?.[0]?.address ?? m.originalInbox).toLowerCase();
    const url = extract(m.text, m.links);
    if (url) urls.set(to, url);
    return;
  }
  if (req.method === 'GET' && req.url?.startsWith('/url?')) {      // JMeter or k6 asks here
    const to = new URL(req.url, 'http://x').searchParams.get('to')?.toLowerCase() ?? '';
    const url = urls.get(to);
    res.writeHead(url ? 200 : 404).end(url ?? '');
    return;
  }
  res.writeHead(404).end();
}).listen(8080);

// After the burst: anything the webhook missed. Newest first; `until` is inclusive, so dedupe by _id.
export async function reconcile(pageSize = 50) {
  let until: string | undefined;
  for (;;) {
    const q = new URLSearchParams({ limit: String(pageSize), ...(until ? { until } : {}) });
    const res = await fetch(`https://mailsac.com/api/domains/${DOMAIN}/messages?${q}`, { headers });
    const page: Array<{ _id: string; inbox: string; received: string;
                        to: { address: string }[]; links?: string[] }> = await res.json();
    for (const m of page) {
      if (seen.has(m._id)) continue;
      seen.add(m._id);
      const text = await (await fetch(
        `https://mailsac.com/api/text/${encodeURIComponent(m.inbox)}/${m._id}`, { headers })).text();
      const url = extract(text, m.links);
      if (url) urls.set(m.to[0].address.toLowerCase(), url);
    }
    if (page.length < pageSize) break;
    until = page[page.length - 1].received;
  }
}

Limits: standard plans and the evaluation

Standard figures are from the pricing page and documentation. Evaluation figures are the fixed allowance of the two-week evaluation as it is configured today; they describe a supervised test, not a guaranteed production rate.

Standard paid plans High-throughput evaluation
Receiving
Incoming rate No published rate. All inbound mail is throttled; verified private domains and private addresses have a higher threshold than public inboxes. The throttling guide says most paid customers never notice it, with the exception of those sending thousands of messages a minute 50 incoming messages a second (a token bucket of 50), one recipient per message
What happens at the limit Two stages: delivery is slowed, by up to about a minute in the worst case, then the SMTP server answers 421 and your sender must retry later. Messages are delayed, not silently dropped, but a two-minute burst can take much longer to land Up to 6,000 recipient admissions in a two-minute burst, then a one-minute cooldown. A minute with no mail resets the burst. Anything over the allowance gets an explicit SMTP error, never a silent drop
Message size 2.5 MB 100 KiB of raw message, including delivery headers. Confirmation emails are usually a few KiB; check yours
Storing and reading
Stored messages 1,000 on Indie, 5,000 on Business, 10,000 on Business+, 12,000 or more on Enterprise; 5,000 more for $10 a month. The oldest messages on private addresses and domains are recycled when the limit is reached 12,000 retained, under the same recycling rules; 100,000 stored deliveries for the whole two weeks. Deleting messages does not give that allowance back
Getting each message Webhook, Slack and forwarding on every plan; WebSocket per address on every plan and for a whole domain on Business and up; REST on every plan Webhook, plus REST text and raw retrieval. WebSocket, Slack, forwarding, public inboxes and the count-only feature are not included
Webhook delivery Best effort: no retry queue. Reconcile over REST for anything that matters Best effort, with a two-second response timeout, no redirects and bounded concurrency. A successful SMTP response means the message is stored, not that the webhook was processed. Reconcile over REST
REST No published requests-per-second limit. Every call is an Op against the monthly allowance 100 requests a second; 300,000 requests and webhook attempts over the evaluation
Getting it
Account and domain Your existing account; one or more private domains by plan A new, dedicated account with no existing plan, plus a verified private custom domain and a webhook receiver. Started by Mailsac staff on request; runs 14 days from the start
Price Business $89 a month or $840 a year; Business+ $199 or $1,900; Enterprise from $7,500 a year for 2 million Ops, up to $24,000 for 20 million, then $1,000 a year per extra million No charge for the two weeks. Ongoing burst throughput is an Enterprise arrangement (the pricing page lists it as “on request”), agreed before the evaluation ends
Guarantee None published None. The evaluation is where the numbers for your workload get measured

Reviewed September 29, 2026. Sources: Mailsac pricing, email throttling, message storage, Ops usage, webhooks and the API reference. Evaluation figures are the current configured allowance and may change; ask for the current limits when you request one.

Sizing your run: Ops and retention

Two numbers matter. Ops are the monthly allowance: one Op is one email received on a private address or domain, one API call, or one push to a webhook, Slack or WebSocket. Stored messages are how many the account keeps at once before the oldest are recycled.

With the webhook pattern, budget 2 Ops per email: one to receive it and one for the push. The Ops documentation says the first push of a message is not charged as an additional Op, so 2 is an upper bound. Reconciliation adds one Op per listing page and one per text fetch, so a run that misses 1% of 4,000 webhooks spends about 40 more. Emptying the domain afterwards is one Op. If each virtual user polls its own inbox instead, count 3 to 6 Ops per email: the receive, one to three list calls two seconds apart, the text read, and an optional delete.

Ops for the run Stored messages and plan fit
1,000 emails About 2,000 with a webhook; 3,000 to 6,000 if every virtual user polls Fits Business (5,000) with room to spare. On Indie (1,000) the run alone fills the allowance, so delete as you go or the oldest messages recycle mid-run
4,000 emails About 8,000 with a webhook; 12,000 to 24,000 polled. Business’s 500,000 Ops covers a webhook run of this size about 60 times a month on Ops alone Fits Business (5,000) one run at a time, if the previous run has been deleted. The rate is the constraint, not the quota: 4,000 in two minutes is about 33 a second, which needs the evaluation
5,000 emails About 10,000 with a webhook; 15,000 to 30,000 polled Equals Business’s 5,000 exactly, so any other mail on the account pushes the oldest run messages out. Business+ (10,000) fits; or reconcile during the run and delete as you go
10,000 emails About 20,000 with a webhook; 30,000 to 60,000 polled Over Business; equals Business+. Enterprise (12,000 or more), a 5,000-message add-on, or deletion during the run. On the evaluation, within its 12,000 retained; two such runs use a fifth of its 100,000 deliveries

Ops estimates are arithmetic from the Op definition on the pricing page, not published figures. Counts reset on the 1st of each month (UTC); you are warned at 80% and service continues to 125% of the plan before it pauses.

Frequently asked questions

Can a Business plan receive 40 to 50 emails a second?

Not without throttling. 40 to 50 a second is 2,400 to 3,000 a minute, which is the range the throttling guide describes as the exception for paid domains: delivery is slowed and, if the burst continues, the server answers 421 until your sender backs off. The monthly Ops and stored-message allowances on Business are not the problem; the rate is. Spread the same run over ten minutes and it is ordinary use. Keep the two-minute burst and request the evaluation.

How do I know whether my run is being throttled?

Watch two things at the sender: time from send to arrival climbing past a few seconds toward a minute, and 421 responses in your provider’s or SMTP client’s logs. On the Mailsac side, compare the number of messages listed on the domain over REST with the number your load generator sent; a gap that closes over the following minutes is delay, a gap that never closes is a sender that gave up on retries. Message logs on Business and up show each arrival.

Do I have to create thousands of inboxes before the run?

No. On a private domain, any address receives mail and becomes an inbox when the first message arrives; there is nothing to reserve and no per-address cost. Generate the address in the load generator, and put one webhook on the domain’s catch-all so it covers every address you will ever generate. Public @mailsac.com inboxes are not suitable for a load test: they are readable by anyone, throttle earlier and keep only a handful of messages per inbox.

Webhook, WebSocket or polling?

Webhook. It is one POST per message to a receiver you control, it works with any load tool over plain HTTP, and it is the delivery method the evaluation supports. Whole-domain WebSockets are available on Business and up and suit a single long-lived consumer, but a dropped socket during a burst loses everything sent while it reconnects. Polling per virtual user multiplies both API traffic and Ops by the number of users and is the slowest to notice a message.

What happens if a webhook is missed?

The message is still stored; only the notification was lost. Webhook delivery is best effort, with no retry queue, and a receiver that is slow, redirects or returns an error counts as a miss. That is why the receiver above records message IDs and the reconcile step lists the domain with GET /api/domains/{domain}/messages, fetches the text of any ID it has not seen, and extracts the link from it. Run reconciliation at the end of every burst, and treat a run as complete only when every generated address has a URL.

When should I use the count-only Load Testing feature instead?

When the question is about your sending side and nobody needs to read the mail: how many messages a minute your provider, queue and templates can push, and whether any fail. Create a load test in the dashboard, send to any address on the temporary loadtester.msdc.co domain for up to eight hours, and read the counts. It is included on Business and up and each arrival is one Op. It cannot return a confirmation URL, because bodies are not stored; for that, use a private domain as described on this page.

Request a high-throughput evaluation

Email support@team.mailsac.com with the account you will use (a new one, with no existing plan), the private domain, your webhook receiver’s URL, the typical message size and how quickly each link must be available. We configure the two-week evaluation, share the current limits and supervise the first representative run.

Runs that fit inside standard throttling need no evaluation: Business starts at $89 a month. Enterprise adds burst throughput for load tests on request.