Shipping · 2 min read · 21 October 2026

Log first, email second

When a form sends an email, the email is the fragile part. Rate-limit, write the request to the database, then send the email and record how it went. A provider outage should never cost you a lead.

A cartoon robot mail carrier stamping a letter in an open ledger while posting another into a mailbox, under a storm cloud over the post office
1
row written before any email is sent
3
email states on that row: ok, failed or skipped
1
idempotency key per request

01 · The failure mode

The email provider will be down at some point.

The tempting version of a contact form is one step: take the form, call the email API, return "Thanks". When that API times out, bounces or hits a quota, the visitor still sees a polite message, and the lead is gone. Nobody knows it existed.

02 · The order

Check, store, send, then record.

  • Validate the input and rate-limit the caller before writing anything. A public endpoint that sends mail from your domain can burn your quota and your sender reputation.
  • Write the request to the database. If this fails, tell the visitor it failed.
  • Send the email.
  • Write back how the email went.

03 · On this site

The request form on this site works exactly like that.

The row ends up with one of three email states: ok, failed or skipped. skipped is for local development, where there's no email key: the row still lands, so the flow is testable without sending anything.

await env.DB.prepare(
  "INSERT INTO requests (id, name, email, reason, locale) VALUES (?, ?, ?, ?, ?)",
).bind(id, name, email, reason, locale).run();

// Logged either way, so a failed email never loses a request
let emailStatus = "skipped";
if (env.RESEND_API_KEY) {
  const ok = await sendEmail(env, { to: env.OWNER, replyTo: email, ...mail,
    idempotencyKey: `request-${id}` });
  emailStatus = ok ? "ok" : "failed";
}
await env.DB.prepare("UPDATE requests SET email_status = ? WHERE id = ?")
  .bind(emailStatus, id).run();

04 · Idempotency keys

A retry shouldn't send two emails.

Every request gets an id before anything else happens, and the email call reuses it as the provider's idempotency key. If a call is retried after a timeout, the provider sends one email, not two.

05 · The same pattern, three times

I keep reaching for it because it keeps being right.

  • On a creative agency's site, a visitor requests the agency's credentials deck. The request is rate-limited, then saved as pending through a database function, then emailed with the PDF attached, then flipped to sent or failed. The database function exists because the public key isn't allowed to read the table back, so a plain insert-then-select would fail.
  • On a product brand's site, contact inquiries are stored with an email log, and each email address can send at most 3 inquiries per hour.
  • On this site, as above.

06 · What's missing

There's no automatic retry yet.

A failed row stays visible in the log, and that's the point: nothing disappears quietly. A small scheduled job that resends failed rows would be the next step. I haven't needed it yet, so I haven't built it.

Next storyDoes AGENTS.md change what a coding agent builds?