Build in public · · 6 min read
I replaced Postmark with a pigeon
Twenty client domains made Postmark's per-domain math ugly, so I built Balodis: a multi-tenant transactional email service on Amazon SES. One day, 110 tests, about $13 a month. With an interactive calculator so you can run the build-vs-buy numbers yourself.
Transactional email is the mail nobody thanks you for: password resets, order confirmations, the invoice PDF. I send it for every client site I build and operate, which means many sending domains, which means I finally read Postmark’s price list the way an accountant reads it. Basic allows 5 sending domains. Pro allows 10. Past that you are on Platform: $18 a month plus $1.20 per extra 1,000 emails, and DMARC monitoring is another $14 per domain per month, so twenty domains pay $280 a month for someone to watch DNS records. Amazon SES charges $0.10 per 1,000 and has never heard of a per-domain fee.
So I built the thing I kept not building. Balodis (Latvian for pigeon) is a multi-tenant transactional email service on SES: every domain I operate, one €10.69 Hetzner box, my own safety rails. The pigeon delivers.
- 1
- day, empty repo to deployed
- 110
- unit tests
- $13
- a month total, all domains
- 12x
- marginal price gap
From the build log, not a pitch deck. The $13: one Hetzner box plus SES pennies, covering every domain at once. The 12x: Postmark Platform charges $1.20 per extra 1,000 emails, SES charges $0.10.
What Postmark actually sells
Calling the SES API is an afternoon. That was never the product. Postmark’s product is everything around the send: bounce and complaint handling, suppression lists, an activity feed your support person can read, per-client isolation, and a deliverability team whose job is keeping the shared IPs clean. SES gives you almost none of that. It is a firehose with a reputation meter, and if your bounce rate crosses 5%, the meter is AWS deciding whether your account still sends tomorrow.
So the build day was not spent on sending. It was spent on rails:
- Per-tenant circuit breakers. Warn at 2% bounce / 0.05% complaint over a rolling day, auto-pause the tenant at 3% / 0.08%. The pause lands at SES level, not just in my database: a paused tenant cannot send even if my app has a bug.
- A kill switch that stops the whole account in one call, because at 2am you want one lever, not a runbook debate.
- Suppression checked on every send, mirrored from SES bounce events, so an address that hard-bounced for one client is not quietly burned by another client’s mail.
- A 45-day activity feed with the rendered email stored. That window is deliberate: it is Postmark’s own retention default, and the feed is the feature clients actually ask about (“did the reset email go out or not?”).
- DNS onboarding that hands out every record at once: three DKIM CNAMEs, a custom return-path subdomain (Outlook filters the default one), and a DMARC record.
Read that list twice before you build. It is the honest price tag, and the calculator below does not include it.
Run your own numbers
Postmark’s pricing has two levers that matter. The domain caps push you up tiers regardless of volume, and every extra 1,000 emails costs 12 times SES list price. The sliders make both visible.
11 or more domains: Platform only
flat server cost, $0 per domain
$26.50/mo stays with you · $318 a year
Postmark list prices, August 2026: Basic $15 (5 domains), Pro $16.50 (10), Platform $18 (unlimited), each including 10,000 emails, then $1.80 / $1.30 / $1.20 per extra 1,000. DMARC monitoring from $14 per domain per month. Self-hosted line: one Hetzner CX33 (about €10.69 with backups and an IPv4) plus Amazon SES at $0.10 per 1,000 in eu-central-1. Not priced in: your ops time. It is not zero, whatever the slider says.
Three things fall out every time I drag these:
- The domain trap is volume-independent. Eleven domains sending almost nothing is still Platform money. On the SES side a domain is a free identity; twenty of them cost what one costs.
- The DMARC add-on is the quiet multiplier. At twenty domains it outbills the actual sending. A free external DMARC report reader does the same job.
- Below about 10,000 emails a month, the saving is coffee. The build case down there is domains, EU data residency, and owning your message log. Not dollars.
The bug I shipped on day one
Idempotency keys. A client sends a key with each request; if the same key arrives again, you return the original result instead of sending twice. I wrote the obvious version: insert the message row with its key, call SES, replay the stored row on any repeat. Then a send failed after the insert. The row existed, the key was recorded, and my replay logic served that dead row to every retry. Forever. The caller did everything right, retried, got a success response, and the email did not exist. An email service that says “sent” and means “no” is worse than no email service.
The fix: a failed send now releases the key, and an exhausted SES throttle comes back as an honest 429 with a Retry-After header instead of a fake success. One gap survived into this post’s first draft: crash in the gap between the insert and the SES call and the row still stuck with its key burned. Writing the paragraph you are reading shamed me into building the reconciler the same day. A job now sweeps queued rows older than 15 minutes and asks the event stream what actually happened: no SES evidence means the send never left, so the row fails and the key comes free; evidence means ingestion hiccupped, so the event gets re-applied. The remaining honest trade: one rare race can produce a visible duplicate, which I will take over a silent loss every time. Day one ships, day three debugs day one, and the blog post debugs day three.
Should you build this?
Buy Postmark if you run one product with a handful of domains and no appetite for servers. $15 a month for their bounce handling and their deliverability people is excellent insurance, and I recommended exactly that for years.
Build when the shape of your business is many small senders: ten or more domains, clients who ask where their data lives (mine lives in eu-central-1 and a Postgres I can pg_dump), and an existing habit of running your own boxes. The economics improve with every domain you add, because SES has no per-domain line and Postmark’s model is the per-domain line.
My evidence boundary, stated plainly: Balodis carries agency-scale volume. Past 50,000 emails a day I would be warming a dedicated IP and re-reading Postmark’s pitch with more sympathy.
Balodis is a devil.services product with exactly one distribution channel: being my client. You cannot sign up for it, and that is deliberate. The safety model (one shared account, per-tenant breakers) only works because I control every sender on it, so it carries mail for the sites I build and run, and nothing else. If I run your site, your password resets ride the pigeon. For everyone else the math is public and the sliders are up there: if your email bill has a per-domain line in it, you now know what the pigeon knows.