Demo, all content is generated
Question

Stripe keeps retrying my webhook, I think because sending the receipt PDF takes too long

Solved · 299 views · asked by nightowl_nina · edited

My webhook on checkout.session.completed does: update db, generate a PDF invoice with a library, upload to storage, send email via Resend. Stripe dashboard shows some deliveries as "Timed out" and then retries, so customers get 2-3 emails.

Vercel logs show the function takes 8-15 seconds sometimes.

What I’ve tried

Increased maxDuration to 60 on Vercel. Still retries sometimes, and duplicates are worse now.

Comment
What's the maxDuration and are you awaiting the email send inside the handler? rafa_dev · edited

3 answers

Marked as helpful by the asker
rafa_dev · edited

Stripe wants a quick 2xx; it doesn't care that your function could run 60s. Its client times out and retries. Make the webhook do the minimum synchronously and push the slow stuff after the response.

On Next.js 15 you can use after():

import { after } from "next/server";

export async function POST(req: Request) {
  const event = verify(await req.text(), req.headers.get("stripe-signature"));

  const isNew = await recordEvent(event.id); // insert ... on conflict do nothing
  if (!isNew) return new Response("dup", { status: 200 });

  await markOrderPaid(event);                 // fast DB write
  after(async () => {
    await generateAndSendInvoice(event);      // slow, runs after response
  });
  return new Response("ok");
}

The recordEvent part fixes the triple emails too: dedupe by event.id before doing side effects.

If the PDF step gets heavier or needs retries, move it to a proper queue. But after() covers this case.

Comment
Deployed. No timeouts since this morning, and the dedupe table already caught 2 retries. thank you! nightowl_nina · edited
Nice, didn't know after() was stable now. oksana_k · edited
clara_w · edited

Also: Stripe can send receipts itself (Settings > Emails). Might not need your own PDF at all.

Comment
lucia_fer · edited

Resend accepts an Idempotency-Key header on send. Use the Stripe event id as the key and a retry can't send the email twice, even if your own dedupe misses.

Comment
customers need a specific invoice format for their bookkeeping, so the PDF has to stay. but good to know nightowl_nina · edited