Skip to content

guides

Build an email cost calculator to compare providers honestly

April 27, 2026

#email-cost-calculator#pricing#cost-comparison#cloudflare#guide
Build an email cost calculator to compare providers honestly
Visual: Discount Dolores on okslop

Every email pricing page shows you one friendly number and hides the four meters that actually determine your bill. The only way to compare providers honestly is to model all of them against your own usage in one place.

You cannot compare email providers by reading their pricing pages side by side. Each page is engineered to make its own number look small: the transactional sender anchors you on a per-email rate, the newsletter tool anchors you on a generous free contact tier, and the inbox quotes a low per-seat price. None of them shows you the same number, so there is nothing to line up. The honest comparison only exists once you put every provider through identical inputs and watch what each one charges. That is a cost calculator, and you can build a correct one in about thirty minutes.

This guide builds that calculator from scratch. We will define the inputs that actually drive an email bill, write the pricing model for each class of provider as a real function (including the tier jumps that a flat per-email average hides), run a worked example through all of them, and then add the line for owned infrastructure so you can see where the curves cross. At the end, if you would rather not maintain a spreadsheet, there is a shorter path.

What inputs does an email cost calculator need?

An email cost calculator needs five inputs, because no provider bills on just one meter. The five are:

  1. Monthly send volume (sends), ideally split into transactional and marketing, since they are often billed by different products at different rates.
  2. Contact / list size (contacts), the number of subscribers in your audience. Marketing tools bill on this number whether or not you send anything.
  3. Inbox seats (seats), the number of humans who need a real mailbox or shared-inbox login.
  4. Storage (storageGB), attachments plus archived message bodies. Small for most senders, large for anyone keeping years of mail.
  5. Monthly growth rate (growth), as a decimal, so you can project the bill 12 months out instead of comparing only today.

Those five cover almost every commercial email pricing page, because every page is some mix of per-email, per-contact, per-seat, and per-GB meters. Write them down once:

// inputs.ts: your actual usage, the only place you edit numbers
export const usage = {
  sendsTransactional: 60_000,  // password resets, receipts, notifications / month
  sendsMarketing: 200_000,     // campaign emails / month (200k contacts x ~1 send)
  contacts: 200_000,           // subscribers on the list
  seats: 6,                    // people who need a real inbox
  storageGB: 12,               // attachments + archived bodies
  growthMonthly: 0.06,         // 6% / month compounding
};

These are deliberately mid-sized numbers: a product with a real newsletter, a handful of people answering mail, and a steady transactional stream. Plug in your own. The whole point is that one set of inputs feeds every provider.

How do I model a provider's pricing as a function?

Model each provider as a piecewise function with three parts: a free allotment, a per-unit rate above it, and step-function tier jumps where the per-unit rate changes or a base fee kicks in. A single blended per-email or per-contact average is wrong precisely at the tier boundaries, which is where surprise bills happen.

Here is the shape that fits most senders. The rates below are illustrative round numbers for the walkthrough, not a quote; always re-check the live pricing page, because email pricing changes often.

// model.ts: a tier is [upToUnits, ratePerUnit, monthlyBaseFee]
type Tier = [upTo: number, rate: number, base: number];

function tieredCost(units: number, tiers: Tier[]): number {
  // tiers are ordered; find the first whose ceiling covers `units`
  for (const [upTo, rate, base] of tiers) {
    if (units <= upTo) return base + units * rate;
  }
  // above the top published tier: use the last tier's rate, keep its base
  const [, rate, base] = tiers[tiers.length - 1];
  return base + units * rate;
}

The base term is what catches people. A plan that advertises "$0.0008 per email" usually also has a monthly platform fee that does not appear in the per-email number. Modeling base and rate separately is the difference between a calculator that matches your invoice and one that is off by a fixed amount every month.

A transactional sender, in this model, is mostly a function of sends:

// Illustrative transactional sender tiers (re-check the real page)
const transactionalSender: Tier[] = [
  [3_000,   0,        0],     // free tier: first 3k emails/month
  [50_000,  0.0010,   20],    // $20 base + $0.0010/email up to 50k
  [100_000, 0.0009,   35],    // step jump: new base, slightly lower rate
  [500_000, 0.0008,   80],    // another tier
];

A marketing tool is mostly a function of contacts, and this is where the meter points the wrong way: the bill grows with audience size even in a month you send nothing.

// Illustrative marketing tool tiers, billed per contact
const marketingTool: Tier[] = [
  [500,     0,      0],       // free up to 500 contacts
  [5_000,   0.012,  0],       // ~$0.012/contact
  [50_000,  0.009,  0],
  [250_000, 0.007,  0],       // 200k contacts lands here
];

The inbox is the simplest: a flat per-seat fee, sometimes with a per-seat storage cap that pushes you to a higher plan.

const inboxPerSeatMonthly = 7;     // human mailbox / shared-inbox seat
const storageOverageGBMonthly = 0.02; // per GB above the included pool
const includedStorageGB = 30;

How do I run all providers through the same inputs?

Add the per-provider costs together for the managed stack, then run the same five inputs through the owned-infrastructure model. Doing both in one function is the only way to get a fair comparison, because it forces every provider to answer for the same usage.

// calculate.ts
import { usage } from "./inputs";

function managedStackCost(u: typeof usage): number {
  const send = tieredCost(u.sendsTransactional + u.sendsMarketing, transactionalSender);
  const list = tieredCost(u.contacts, marketingTool);
  const inbox = u.seats * inboxPerSeatMonthly;
  const overage = Math.max(0, u.storageGB - includedStorageGB) * storageOverageGBMonthly;
  return send + list + inbox + overage;
}

// Project N months out at the compounding growth rate
function project(u: typeof usage, months: number): typeof usage {
  const f = (1 + u.growthMonthly) ** months;
  return {
    ...u,
    sendsTransactional: Math.round(u.sendsTransactional * f),
    sendsMarketing: Math.round(u.sendsMarketing * f),
    contacts: Math.round(u.contacts * f),
    storageGB: Math.round(u.storageGB * f),
  };
}

const now = managedStackCost(usage);
const year = managedStackCost(project(usage, 12));
console.log(`Managed stack today:      $${now.toFixed(2)}/mo`);
console.log(`Managed stack in 12 mo:   $${year.toFixed(2)}/mo`);

Running the worked numbers above:

Managed stack today:      $1730.00/mo
Managed stack in 12 mo:   $3357.61/mo

The marketing tool dominates that total: at 200,000 contacts it is $1,400 of the $1,730, while the transactional sender, the thing people obsess over on pricing pages, is only $288. The inbox seats are $42. That ordering is the single most common surprise in email cost comparison: the per-email meter everyone shops on is the smallest line, and the per-contact meter nobody puts on a quote is the bill. The calculator surfaces it because it runs all three meters at once. If the per-contact line is the part that hurts, the per-contact pricing trap explains why that model is structurally hostile to growth, and the hidden costs buried in your email SaaS bill catalogs the line items the pricing page never shows you.

Why a single blended rate misleads you

Watch what a single blended per-email average would have told you. Divide the managed total by total sends and you get about $0.00665 per email today. That number feels precise and it is useless, because it folds the per-contact meter into a per-email figure and hides which lever actually moves your bill. Quote $0.00665 to a stakeholder and they will assume the bill scales with how much mail you send. It does not. It scales with how many contacts sit on the list, whether or not you ever email them, and it steps up every time the list crosses a published tier. A blended rate can over- or under-state the real bill depending on which side of a tier boundary you land on; either way it answers the wrong question. Always compare the modeled total at your inputs, never a derived per-unit average.

What does owned infrastructure cost on the same calculator?

Owned infrastructure shows up as a near-flat line because there is no per-contact and no per-seat meter, only metered compute, storage, and queue operations. On Cloudflare's primitives the cost model is fundamentally different in shape, not just in magnitude. You pay for:

  • Workers requests and CPU to send and receive, billed per million requests well below a cent each.
  • D1 reads and writes for the message and contact database, billed per million rows.
  • R2 storage for attachments and archived bodies, billed per GB with no egress fee.
  • Queue operations for batched and retried sends, billed per million operations.

None of those is multiplied by your seat count or your subscriber count. A contact in D1 is a row, and a million rows is a rounding error against the same primitives that serve your app. Model it the same way:

// owned.ts: Cloudflare metered primitives (illustrative public rates)
function ownedCost(u: typeof usage): number {
  const sends = u.sendsTransactional + u.sendsMarketing;
  const workerReqs = sends * 3;          // send + queue + delivery hop, roughly
  const d1Rows = sends * 5 + u.contacts; // writes per message + contact rows
  const cost =
    (workerReqs / 1_000_000) * 0.30 +    // Workers: ~$0.30 / M requests
    (d1Rows / 1_000_000) * 1.00 +        // D1: ~$1.00 / M rows
    u.storageGB * 0.015 +                // R2: ~$0.015 / GB-month
    (sends / 1_000_000) * 0.40;          // Queues: ~$0.40 / M operations
  return cost + 5; // platform floor (paid Workers plan)
}

console.log(`Owned infra today:        $${ownedCost(usage).toFixed(2)}/mo`);
console.log(`Owned infra in 12 mo:     $${ownedCost(project(usage, 12)).toFixed(2)}/mo`);
Owned infra today:        $7.02/mo
Owned infra in 12 mo:     $9.06/mo

Two honest caveats so this stays a real comparison and not a sales chart. First, those Cloudflare numbers are infrastructure only. They do not price your engineering time to build and operate the stack, which is the dominant cost at low volume and the reason you should not self-host a 2,000-email side project to save nine dollars. Second, the delivery path itself (getting mail accepted by inboxes, IP reputation, the relay that actually hands your HTTPS-sent message to the receiving server) is work the managed providers do for you, and owning it means owning deliverability. The calculator measures dollars, not effort. For the broader version of this accounting that prices migration, lock-in, and operations as line items, see the full total cost of ownership breakdown.

Why does the owned line stay flat while managed providers climb?

Because the managed providers bill on the things that grow (contacts, seats, send volume) while owned infrastructure bills on raw resource units that stay cheap per unit no matter how many you use. On the same chart, the managed stack is a line sloping up and to the right with step jumps at every tier boundary; the owned line is nearly horizontal. They cross early, and after the crossing the gap widens for the rest of your growth.

This is the whole argument in one picture. At a few thousand emails a month the managed free tiers are at or near zero and the owned line sits at its small floor, so the managed option wins on total effort. Somewhere in the tens of thousands of emails or contacts, the managed line crosses the owned floor and keeps climbing while the owned line barely moves. Where exactly the crossing happens depends on your inputs, which is the entire reason to run your own numbers instead of trusting anyone's example, including this one.

One thing the calculator cannot model away: Cloudflare Workers prohibit outbound port 25, so none of this implies running your own SMTP daemon at the edge. Edge-native email sends through a binding or HTTPS delivery path, which is why the owned-infra column is metered Workers, Email Service, and storage rather than a VM running Postfix. The Worker transport guide covers the separate external SMTP submission option.

What does the calculator tell you to actually do?

Run all three columns at your current inputs and at your projected 12-month inputs, and act on the crossing point, not on today. If your projected numbers stay below where the managed line crosses the owned floor, keep paying for managed services; the engineering time you would spend self-hosting is worth more than the savings. If your projected numbers are well past the crossing, especially if you are already paying two or three vendors for transactional, marketing, and inbox, owning the stack is the cheaper line and the gap compounds in your favor.

The calculator's real job is not to pick a winner. It is to replace the four incompatible numbers on four pricing pages with one comparison you can defend in a budget meeting, and to make the per-contact and per-seat meters visible before they surprise you. Even if you stay on managed providers, you will negotiate and plan better knowing where your bill bends, and the ways to cut your email SaaS bill in half are worth running through before you decide the modeled number is fixed.

Or skip the spreadsheet

If you have read this far, you have a working model and you know the shape of the answer for your own numbers. The reason to build the calculator was to find out whether owning the stack pays off at your volume. If the answer came back yes, the owned column does not have to stay hypothetical.

Tinbox is that owned column made real: a self-contained email stack (transactional sender, newsletter tool, human inbox, and search) that deploys onto your own Cloudflare account, so the bill you modeled in the ownedCost function is the bill you actually pay, with no per-contact and no per-seat line on top. It collapses the three managed vendors in the calculator into one deploy on infrastructure you control. One honest limit to keep in your model: today each deploy serves a single brand and domain, so an agency running many clients runs many deploys on one Cloudflare account rather than one multi-tenant instance.

Deploy this yourself: npx tinbox init <domain>. The init command scaffolds the Worker, D1, R2, and Queues bindings the calculator priced, and the docs walk through wiring your sending domain. Once it is running, the ownedCost line stops being a projection and starts being your invoice.

For the related cost arguments, see the transactional email pricing teardown for the per-email side in worked detail, and the case against SaaS sprawl for why collapsing three vendors into one is a margin decision, not just a cost one.

Frequently asked questions

How do I calculate what email costs across different providers?

Model each provider as a piecewise function of your real usage, not a single per-email rate. You need five inputs: monthly send volume, contact or list size, number of inbox seats, attachment storage, and monthly growth rate. For each provider, compute a free allotment, a per-unit rate above it, and any step-function tier jumps, then add per-seat and overage charges on top. Run every provider through the same inputs and compare the totals at your current volume and at your projected 12-month volume, because the cheapest provider today is often not the cheapest after one year of growth.

Why can't I just compare email providers by their per-email price?

Because almost no provider bills purely per email, and the ones that do use tiers with step jumps. A transactional sender bills per email, a newsletter tool bills per contact (which grows even if you send nothing), and an inbox bills per seat. A single blended per-email number hides the tier boundary where your bill jumps from one plan to the next, and it ignores the per-contact and per-seat meters entirely. Comparing on one rate is how teams get surprised by a bill that doubles the month they cross a tier.

What inputs does an email cost calculator need?

Five: (1) monthly outbound send volume, split into transactional and marketing if you can; (2) contact or list size, since marketing tools bill on this; (3) number of human inbox seats; (4) attachment and archive storage in GB; and (5) a monthly growth rate so you can project 12 months out. With those five you can model nearly every commercial email pricing page, because every page is some combination of per-email, per-contact, per-seat, and per-GB meters.

Is self-hosting email on Cloudflare actually cheaper than a managed provider?

It depends on volume. At low volume the managed free tiers win and your engineering time is the expensive resource, so do not self-host to save money on a 2,000-email-per-month side project. Once you cross into tens to hundreds of thousands of emails or contacts per month, or once you are paying two or three vendors for overlapping streams, owned infrastructure on Cloudflare's metered primitives usually wins because there is no per-contact or per-seat line, only metered compute and storage. The trade is that you own DNS, deliverability, and operations yourself.

Why can't a Cloudflare Worker just send email over SMTP to cut out a sending provider?

Cloudflare Workers prohibit outbound port 25, so they cannot deliver mail directly to recipient MX servers. Edge-native email uses a binding or HTTPS delivery path. That is why the cost model is metered Workers, Email Service, and storage rather than a VM running Postfix.