← Blog

Engineering

Why we run our MTA on a separate machine from the API

A short note on the architectural choice that lets us update one without redeploying the other.

FA
Fahad Alarifee
Founder
April 30, 2026 · 4 min read

When we started, the API and the MTA were the same Go binary. We deployed it to Railway, set EMAIL_PROVIDER=relay on one and EMAIL_PROVIDER=direct on the other, and called it a day.

A week in, we had a problem: every time we wanted to ship a tiny analytics tweak on the dashboard, we redeployed the box that was actively talking to Gmail over port 25. That meant a 30-second window where every send failed.

Two binaries, one repo

So we split it. apps/api is the dashboard, the public REST surface, and the database client. apps/mta is a 36 KB binary whose only job is to take a message over HTTPS, sign it with DKIM, and deliver it via SMTP.

Now we ship api updates in seconds. We ship mta updates with one ssh + docker compose up. The two reach each other through a shared bearer token — no shared database, no shared secrets table, no shared anything except the network handshake.

A pool, not a singleton

Then we made the API talk to a pool of MTAs instead of one. Today every send picks an MTA at random, weighted, with a circuit breaker so a flapping box drops out of rotation automatically. Adding a second MTA in a different country took us five minutes from clicking "buy" on Cybrohost to seeing real traffic flow through it.

Most outage stories end with "and so we added retries." Ours starts with "we never had to."

Continue reading