All posts
The Amazon SES alternative for developers
Gabe 13 min read

The Amazon SES alternative for developers

SES is the cheapest way to send and the most assembly-required way to receive: sandbox approval, then raw MIME in S3 behind SNS, Lambda, and IAM. MailKite (which we build) sends the moment DNS verifies and delivers inbound as one parsed JSON webhook. Honest comparison, working code, runnable demo.

Here is that difference in one picture: the same inbound email, and everything you operate to receive it on each side. The rest of the post is the honest version of this diagram — where SES genuinely wins, what it costs you in plumbing, and the 25 lines of working code (from a runnable demo repo you can open in your browser) that are the entire MailKite side.

Amazon SES sender SESreceipt rule S3raw MIME SNS Lambdafetch + parse MIME your app …plus the IAM roles, receipt-rule regions, bounce/complaint SNS topics, and suppression list you build and keep alive ← yours to write and operate MailKite sender MX edgeparse + auth JSON webhooksigned, retried, replayable your app the 25 lines below are the whole "your app" integration
Receiving one email: what you operate on SES vs on MailKite. Same input, two very different pipelines.

Here is the entire MailKite receiving pipeline. Not a fragment: this runs as pasted on Node 18+, one dependency (npm install mailkite).

// minimal.mjs — the whole pipeline. Full version + tests: github.com/mailkite/demo-amazon-ses-alternative
import { createServer } from "node:http";
import { MailKite } from "mailkite";

const SECRET = process.env.MAILKITE_WEBHOOK_SECRET ?? "whsec_demo_secret";

createServer(async (req, res) => {
  let raw = "";
  for await (const chunk of req) raw += chunk;

  // signature check, replay window, constant-time compare — one call
  if (!MailKite.verifyWebhook(req.headers["x-mailkite-signature"], raw, SECRET)) {
    res.writeHead(401).end();
    return;
  }

  const event = JSON.parse(raw); // already parsed email — no S3, no MIME parser
  if (event.type === "email.received") {
    console.log(event.from.address, "·", event.subject, "·", event.text);
  }
  res.writeHead(200).end("ok");
}).listen(3000);

(Can’t take a dependency? The repo’s raw-server.mjs is the zero-dependency version — hand-rolled HMAC, replay window, constant-time compare. Our first draft of it got the timestamp unit wrong, which is rather the point of the SDK call.)

Run it without an account, in one command: clone the repo and npm start boots the server and self-fires a correctly signed email.received event at its own localhost — no domain needed — and a parsed email lands in your terminal. Open it in StackBlitz (real Node in your browser tab) to watch it run on load. Tamper with a byte and you get a 401.

Where SES wins, honestly

Per email sent, SES is about the cheapest game anywhere, and AWS sending IPs have years of reputation behind them. If you push millions of transactional messages a month, have AWS muscle in-house, and can staff the plumbing, nothing beats its unit economics. This post isn’t “SES is bad.” It’s “SES makes you the integrator,” and for a lot of developers that trade isn’t worth it.

What SES actually asks of you

The low price tag is real; the operational bill is the part nobody quotes you. Receiving one email is a pipeline you build and keep alive, top to bottom:

SES receivingan email arrives receipt rule → S3raw MIME written to a bucket SNS → Lambdaa function you wrote is invoked getObject(S3)fetch the raw bytes back parse MIMEheaders, body, attachments, encodings your app logicfinally, do something with the email Every box above the blue one is yours to build, secure with IAM, and keep running. On MailKite, the blue box is the only box.
The SES receive pipeline, stage by stage. MailKite collapses every gray stage into a signed JSON webhook.

Between “signed up” and “reliably sending and receiving” sits the rest of it too:

  • The sandbox. New SES accounts can only email verified addresses until you request production access and AWS approves it. Approvals are a review, not a switch, and denials often come with "for security purposes, we can't share specifics."
  • IAM, identities, and regions. Verified identities, roles, policies, and per-region setup. SES receiving only exists in some regions at all.
  • A dashboard you build. There's no real inbound console. Visibility means stitching CloudWatch, SNS, and S3 together yourself.
  • Bounce and complaint handling. You subscribe SNS topics, process notifications (usually a Lambda), and maintain a suppression list, because if bounce or complaint rates drift up, AWS pauses your sending.
The sandbox bites at the worst timeProduction-access approval is a human review with no SLA, and a denial rarely tells you why. Teams hit it the week of launch. MailKite has no sandbox: once your domain passes SPF + DKIM, you can send to anyone.

Here’s the SES version of the 25 lines up top, after you’ve built the receipt rules, bucket policy, SNS topic, and IAM role (ses-contrast/handler.mjs in the demo repo):

// SES inbound: receipt rule → S3 → SNS → this Lambda → parse the MIME yourself
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { simpleParser } from "mailparser";

const s3 = new S3Client({});

export const handler = async (event) => {
  const { bucketName, objectKey } = event.Records[0].ses.receipt.action;
  const obj = await s3.send(new GetObjectCommand({ Bucket: bucketName, Key: objectKey }));
  const mail = await simpleParser(await obj.Body.transformToString());
  console.log(mail.from?.text, "·", mail.subject); // attachments, encodings: your job now
};

None of this is exotic if you’re an AWS shop. But it’s a stack of undifferentiated plumbing between you and “an email came in, do something with it.”

The comparison, no adjective inflation

Amazon SESMailKite
Start sendingSandboxed until AWS approvesDNS-verify (SPF+DKIM), then send
Inbound deliveryRaw MIME → S3 → SNS/Lambda → you parseOne parsed JSON webhook
Inbound setupReceipt rules + S3 + SNS/Lambda + IAMOne webhook URL
Bounce/complaint handlingSNS + Lambda + suppression you buildHandled; metered, no surprise pause
Inbound dashboardBuild it (CloudWatch/S3)Logs + one-click replay
Per-email price at scaleAmong the lowest anywhereMetered, transparent
DeliverabilityExcellent AWS IPsSPF/DKIM/DMARC aligned
Human supportPaid AWS Support tierIncluded on paid plans

The through-line: SES wins raw per-email price and world-class sending IPs. MailKite wins time and total cost of ownership: no sandbox wait, no AWS services to run, and inbound that’s already parsed.

What actually hits your webhook

The same inbound email, delivered parsed. No S3 round-trip, no MIME parser, and the auth block means you never re-derive SPF/DKIM/DMARC yourself:

{
  "id": "msg_2Hk9…",
  "type": "email.received",
  "from": { "address": "ada@example.com" },
  "to": [{ "address": "support@myapp.ai" }],
  "subject": "Re: invoice #1042",
  "text": "Looks good — approved!",
  "html": "<p>Looks good — approved!</p>",
  "threadId": "<a1b2c3@mail.example.com>",
  "auth": { "spf": "pass", "dkim": "pass", "dmarc": "pass", "spam": "ham" },
  "attachments": [
    { "id": "msg_2Hk9…:0", "filename": "po.pdf", "contentType": "application/pdf",
      "size": 18213, "url": "https://api.mailkite.dev/att/2Hk9…/0?exp=…&sig=…" }
  ]
}

Don’t take the shape on faith — fire one and watch it come back, auth block and all. Spoof the SPF/DKIM to fail and see the verdict flip:

The same handler exists for Python, Ruby, Go, PHP, and Java: see the receiving docs and webhook security. If you’d rather keep SMTP, the submission edge takes AUTH on :587 too.

Where I won’t overclaim

SES is genuinely excellent at the thing it’s for: cheap, high-deliverability, high-volume sending, operated by teams already fluent in AWS. At millions of messages a month its per-email price is hard to walk away from, and I won’t pretend MailKite undercuts it there. My claim is specific: for the developer experience (starting without a sandbox, and receiving mail as parsed JSON instead of raw MIME in a bucket), MailKite removes the AWS pipeline SES makes you build and maintain. We built MailKite because we kept rebuilding that pipeline; below “dedicated deliverability engineer” scale, the trade pays for itself in the plumbing you don’t write. And you don’t have to leave AWS to take it: the webhook is plain HTTPS, so the receiver can be the Lambda you already have.

FAQ

Is MailKite cheaper than Amazon SES? Per email sent at high volume, no: SES is about the cheapest anywhere. But SES receiving adds S3, SNS, and Lambda costs plus the engineering time to build and run the pipeline, and MailKite has no per-domain fee. MailKite starts free (3,000 messages/mo, in + out), so total cost of ownership favors MailKite until you’re sending at large scale.

Can Amazon SES receive email? Yes, in supported regions: receipt rules deliver raw MIME to an S3 bucket and notify SNS/Lambda, then you fetch and parse the MIME yourself. MailKite delivers the already-parsed message as JSON to your webhook.

What is the SES sandbox? Every new SES account is sandboxed: you can only send to verified addresses until AWS reviews your request and grants production access. MailKite has no sandbox; once your domain passes DNS verification (SPF + DKIM), you can send to anyone.

Do I have to leave AWS to use MailKite? No. MailKite works over a plain HTTPS webhook and REST API: call it from Lambda, ECS, EC2, or anywhere else. You’re replacing the SES receiving pipeline (and the sandbox), not your infrastructure.

Does MailKite match SES deliverability? MailKite sends with aligned SPF/DKIM/DMARC on a monitored transactional stream. SES’s AWS IPs have a long, strong reputation; at very high volume with a warmed dedicated IP, SES has the edge. For typical transactional and inbound-heavy workloads, MailKite’s placement is built to land in the inbox.


If SES has you maintaining a receipt-rule-to-S3-to-Lambda pipeline just to read an email, or waiting on a sandbox approval to send one, there’s a simpler shape. Clone the demo repo (or run it in your browser), then point a domain at MailKite and your next inbound email arrives as parsed JSON. Found a case the demo doesn’t cover? Open an issue.

Related: the full MailKite vs Amazon SES comparison, the pillar on why receiving email is hard, and why deliverability feels like a black box.

Discuss this post: Hacker News Share on X Share on LinkedIn

Related posts