DEV Community

kevindev
kevindev

Posted on

PostgreSQL Leases for Resend Email APIs

Resend-email endpoints break when the API, worker, and database disagree about who owns the next send. PostgreSQL leases make that ownership visible and pretty durable.

Why resend endpoints drift out of sync

The bug usually starts with a reasonable shortcut. A POST /auth/resend-verification route checks whether the user is pending, enqueues a job, and returns 202. Later, support sees duplicate messages, stale tokens, or a resend that claims success while nothing was actually deliverd.

What changed? Normally one of these:

  1. The worker timed out after the provider accepted the request.
  2. The client retried on a slow network.
  3. Another node handled the same resend before the first write fully settled.

When send ownership lives only in process memory, you cannot answer a basic backend question: "which attempt currently owns delivery?" That is why I now model resend work as a leased database record, not a loose side effect.

Lease the send in PostgreSQL, not in app memory

A lease is just a short-lived claim on the next delivery attempt. I store resend work in a table where one row represents one email intent. The worker must atomically move the row from pending to leased before it talks to the provider.

That sounds tiny, but it fixes a bunch of messy edge cases:

  • duplicate sends become far less likely
  • retries can inspect ownership instead of guessing
  • operators can see stuck work with one query
  • API responses stop pretending the mail was already sent

In PostgreSQL, FOR UPDATE SKIP LOCKED is the part that keeps concurrent workers from grabbing the same pending row. It is not magic, but it is very dependable when the rest of the flow stays simple.

A schema that keeps retries boring

I like a table shaped roughly like this:

create table email_delivery_intents (
  id bigserial primary key,
  user_id bigint not null,
  purpose text not null,
  state text not null check (state in ('pending', 'leased', 'sent', 'failed', 'consumed')),
  lease_expires_at timestamptz,
  idempotency_key text not null,
  token_hash text,
  provider_message_id text,
  failure_code text,
  created_at timestamptz not null default now(),
  sent_at timestamptz
);
Enter fullscreen mode Exit fullscreen mode

Then the worker claims work like this:

with next_job as (
  select id
  from email_delivery_intents
  where purpose = 'signup_verification'
    and state = 'pending'
  order by created_at asc
  for update skip locked
  limit 1
)
update email_delivery_intents i
set state = 'leased',
    lease_expires_at = now() + interval '2 minutes'
from next_job
where i.id = next_job.id
returning i.id, i.user_id, i.token_hash, i.idempotency_key;
Enter fullscreen mode Exit fullscreen mode

The key is not the exact SQL. The key is making ownership first-class. If the provider call later fails, I can decide whether to retry the same row, mark it failed, or let the lease expire and recover it. That decision becomes data-driven, which is nice because data ages better than hand-wavy queue logic.

REST API behavior that stays honest

For the API, I avoid saying "email sent" unless a send has actually been recorded. Most resend endpoints should return something closer to "accepted, work is now owned by delivery intent X".

My usual rules are:

  1. Reuse an existing active intent for a short replay window.
  2. Create a new pending intent only when the prior one is consumed or failed.
  3. Never mint a fresh token just because a client got impatent and clicked again.

Here is a compact Node example:

async function requestResend(db, userId) {
  const current = await findActiveIntent(db, userId);

  if (current && ['pending', 'leased', 'sent'].includes(current.state)) {
    return { status: 202, intentId: current.id };
  }

  const created = await createPendingIntent(db, userId);
  return { status: 202, intentId: created.id };
}
Enter fullscreen mode Exit fullscreen mode

This pairs well with an idempotent verification email flow. I also like keeping a small runbook for reclaiming expired leases, the same way frozen plans for automation runs keep cron systems easier to reason about.

One subtle benefit: incident review gets much cleaner. You can ask which resend was pending, which worker leased it, and whether the lease expired before the provider callback arrived. Those are useful backend facts, not vibes.

Where disposable inboxes fit

Disposable inboxes are helpful, but they should verify the contract rather than replace it. When I test resend flows locally, I want proof that one lease produced one effective message, not just proof that some inbox eventually got mail.

A simple test loop looks like this:

  • create a fresh user
  • call resend twice under the same auth context
  • assert only one row is leased or sent
  • verify one inbox receives one valid link

For that last step, a throwaway inbox from tempmailso can be useful during manual checks. I still keep it as a small part of the story, though. If somebody scribbles tamp mail com in a test note, your resend logic should still be deterministic and auditable.

Q&A

How long should the lease last?

Long enough for one provider attempt plus a small buffer. In most apps, 60 to 180 seconds is enough. If you make it too long, recovery gets slow. If you make it too short, healthy sends may get reclaimed early, which is not ideal tbh.

Should I delete failed intents?

Usually no. Keep them for reconciliation and support analysis, then archive later. Failed rows are often the fastest way to explain weird resend behavior from last night.

Is this overkill for a small app?

Not really. The schema is small, the API rules are clear, and the operator experience is much better. Even modest auth systems get weird concurrency at the worst possible moment, so a boring lease model pays for itself pretty quick.

If your resend endpoint keeps acting random under load, do less in memory and more in PostgreSQL. Once delivery ownership is visible, the whole system gets calmer, and honestly a bit easier to trust.

Top comments (0)