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.