Vercel function timeout on video generation: use a webhook

A Vercel Function stops at 300 seconds by default; a Sume video run takes minutes. Submit with a webhook_url, return, then verify the signed POST.

6 min readSume
All posts

To keep a Vercel function timeout from cutting off AI video generation, do not wait for the video inside the function: submit the Sume run from a Next.js route handler with a webhook_url, return at once, and let a second route verify Sume's signed POST when the run ends. Vercel Functions stop at 300 seconds by default, and long-form host video on Sume typically takes 15 to 30 minutes.

Sume facts come from Create a run, Runs and results, Run webhooks, and Verifying webhooks; Vercel and Next.js facts come from their own docs under Sources, all read on 2026-09-27. Sume has no Vercel connector: both routes below are plain HTTPS. Sume's own wait caps and SDK timeouts are in Video generation API timeouts.

How long can a Vercel Function run?

With fluid compute, which Vercel enables by default, every plan starts at 300 seconds. In the App Router you raise the limit for one route with a named maxDuration export, up to your plan's maximum. A function that runs past its maximum duration is terminated.

On the Sume side, runs that make video take minutes, not seconds, and a run still in progress carries an expires_at deadline. Even the 1800-second beta ceiling is shorter than that 90-minute deadline, so a larger maxDuration does not fix the design.

From Vercel's Configuring Maximum Duration and Sume's Formats and Runs and results pages, read 2026-09-27.
LimitValue
Vercel Function default, every plan300 s
Vercel maximum on Hobby300 s
Vercel maximum on Pro and Enterprise800 s; up to 1800 s (30 minutes) per function as a beta, on supported runtimes
Sume long-form host video runTypically 15 to 30 minutes
Sume run deadline, expires_at90 minutes from created_at, or sooner for a run that goes silent; then the run is force-finalized as failed

What happens to the video when my function times out?

Nothing stops on Sume's side: a client-side timeout does not cancel a job or run, which keeps running and billing. The damage is on your side. The function can die before it stores the run id, and a retry without an idempotency key starts a second paid run. Idempotency keys for AI video APIs covers the replay rules; two habits cover this route:

  • Send an Idempotency-Key derived from the thing being made, such as an order id plus a version you bump on purpose, not a fresh UUID per request. A retry with the same key and body returns the original run with idempotency_hit: true, with no second charge.
  • Store data.id from the create response before you return. A fresh run answers 202 and a replay 200; both carry the full receipt.

How do I submit the run from a Next.js route handler?

Build the Sume request in a route handler, so the API key stays on the server. Store it as a Vercel environment variable; Vercel encrypts those at rest, and a change applies only to new deployments. Never put the key in client JavaScript or a NEXT_PUBLIC_* variable. The route sends a spend cap and a communication.webhook_url, which asks Sume for one signed format.run.terminal POST when the run completes or fails.

// app/api/videos/route.ts (runs on the server)
export async function POST(request: Request) {
  const { orderId, productUrl } = await request.json(); // after your own auth check
  const res = await fetch("https://api.sume.com/v1/formats/acme/product-promo/runs", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.SUME_API_KEY}`,
      "Content-Type": "application/json",
      "Idempotency-Key": `order-${orderId}-promo-v1`,
    },
    body: JSON.stringify({
      input: { product_url: productUrl },
      generation_spend_cap_usd: 3,
      communication: { webhook_url: "https://example.com/api/sume-webhook" },
    }),
  });
  if (!res.ok) return new Response(await res.text(), { status: res.status });
  const { data } = await res.json(); // 202 new run, 200 idempotent replay
  await saveRun(orderId, data.id); // your database
  return Response.json({ runId: data.id }, { status: 202 });
}

How do I receive the result without hitting the timeout again?

Sume gives each delivery attempt 10 seconds and retries a slow or failed answer, up to 10 attempts in total. So the webhook route does little: it reads the raw body with await request.text(), the pattern the Next.js route handler docs show for webhooks, checks it with verifyWebhook from @sume-com/sdk, records the event once per request_id (the same on every retry), and answers 204. Signed webhooks for video runs covers the rest of the delivery contract.

// app/api/sume-webhook/route.ts
import { after } from "next/server";
import { verifyWebhook } from "@sume-com/sdk";

export async function POST(request: Request) {
  const body = await request.text(); // raw, before any JSON.parse
  const ok = await verifyWebhook({
    body,
    headers: request.headers,
    secret: process.env.SUME_COM_WEBHOOK_SIGNING_SECRET!,
  });
  if (!ok) return new Response("bad signature", { status: 401 });

  const event = JSON.parse(body);
  if (event.event !== "format.run.terminal") return new Response(null, { status: 204 });
  const fresh = await recordOnce(event.request_id, body); // insert-or-ignore
  if (fresh) after(() => markOrderReady(event)); // runs after the 204 is sent
  return new Response(null, { status: 204 });
}

What should run inside after()?

after() from next/server schedules work after the response is sent, and it has been stable since Next.js 15.1.0. It runs for the platform's default or configured max duration of the route, and on Vercel, Next.js keeps the invocation alive with waitUntil. It is the same function budget, spent after the 204.

  • Good fits: marking the order ready, notifying the user, storing primary_output_url against your record.
  • Poor fits: anything that waits minutes. If the next step is more generation, start another Sume run with its own webhook.
  • A receipt over 1 MiB arrives with payload: null, so fetch it from error.result_url with your API key inside after().

What if the webhook never arrives?

Keep a read of result_url as the backup: GET /v1/format-runs/{run_id}/result returns the full receipt once the run is terminal and 409 run_not_completed while it is in flight. A canceled or skipped run never sends a POST. Register the final URL of the webhook route, because redirects are not followed and a 3xx counts as a failed attempt. Sume webhook not received? shows how to read webhook_delivery and redeliver an event.

Sources

Related posts

More in Integrations

All Integrations posts

Written by Sume