Vercel Function Timeout: Limits, maxDuration and the 504 Fix
A request hangs, then the browser gets a 504 and Vercel's error page says FUNCTION_INVOCATION_TIMEOUT. The obvious reaction is to look for a setting that makes the timeout longer. That setting exists, and sometimes it is the right fix. More often the 504 is telling you something else: the function never sent a response, an upstream call is stuck, or the work does not belong inside an HTTP request at all.
This post covers what the limit actually is today, how to change it in a Next.js 16 App Router project, and how to decide between raising it and moving the work somewhere else. The numbers below were checked against Vercel's documentation in October 2026; they have changed several times, so confirm them before you rely on them.
What the error means
Vercel gives every function invocation a maximum duration. If the function has not finished by then, Vercel terminates it and returns HTTP 504 Gateway Timeout with the code FUNCTION_INVOCATION_TIMEOUT.
Two details in the documentation are easy to miss:
- The clock covers the whole response, including streaming. Vercel's definition of max duration includes "time spent processing the request and sending the response, including streamed responses". A route that streams for six minutes on a five-minute limit gets cut off mid-stream.
- A function that never responds also times out. If a code path forgets to return a
Response(or an unhandled rejection leaves the handler waiting), nothing is sent, and the invocation runs until the limit kills it. The 504 shows up five minutes later and looks like a performance problem when it is a bug.
The current limits
With Fluid compute, which Vercel has enabled by default for new projects since April 23, 2025, the limits for the Node.js, Bun and Python runtimes are:
| Plan | Default | Maximum | Extended maximum |
|---|---|---|---|
| Hobby | 300 s | 300 s | — |
| Pro | 300 s | 800 s | 1800 s (beta) |
| Enterprise | 300 s | 800 s | 1800 s (beta) |
The 1800-second tier is in beta, has to be set per function (not as a project default), and only works on specific runtime versions.
The Edge runtime works differently: it must begin sending a response within 25 seconds and can then keep streaming for up to 300 seconds.
The practical takeaway for Hobby is simple. The default already is the maximum. If your function times out on Hobby at five minutes, no configuration will give it more time. That is the case where you have to change the design, not the setting.
If your project is older and you see a much shorter timeout, check whether Fluid compute is turned on under Settings → Functions. Projects without it run on the older, lower limits.
Setting maxDuration in the App Router
Next.js has a route segment option for this. The Next.js 16 docs describe maxDuration as the maximum execution time in seconds for server-side logic in a segment, which deployment platforms read from the build output. You export it from a route.ts, page.tsx or layout.tsx:
// app/api/report/route.ts
export const maxDuration = 60;
export async function POST(request: Request) {
const body = await request.json();
const result = await buildReport(body);
return Response.json(result);
}Two Next.js-specific points:
Server Actions inherit the page's value. A Server Action does not have its own route file, so you cannot export maxDuration next to it. The docs say to set it at the page level; that changes the timeout for every Server Action used on that page.
// app/dashboard/import/page.tsx
export const maxDuration = 120;
export default function ImportPage() {
// the form below calls a Server Action
return <ImportForm />;
}Cache Components does not remove it. In Next.js 16, enabling Cache Components removes the dynamic, dynamicParams, revalidate and fetchCache segment options. maxDuration is not on that list, so it keeps working either way.
Setting it in vercel.json
For a project-wide default, or for frameworks without a code-level option, use the functions key in vercel.json with a glob:
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"functions": {
"src/app/api/**/*.ts": {
"maxDuration": 30
}
}
}The gotcha is in the path. If your project uses the src directory, Vercel's docs say the pattern has to start with src/, otherwise it matches nothing and the setting silently does not apply. Vercel also warns that a bare **/* glob matches everything under the path, so add a file extension to keep it narrow.
There is a third place to set a default: the Default Max Duration field under Settings → Functions in the dashboard.
Which value wins
When the same function gets a duration from more than one place, Vercel applies this order, highest first:
- The function code (
export const maxDuration) vercel.json- The dashboard setting
- The Fluid compute default
So a dashboard change will not override a value someone exported in the route file. If a timeout does not change after you edit the dashboard, search the code for maxDuration first.
Before you raise it: is the function actually stuck?
Vercel's troubleshooting list for this error is short, and most items are not "the work is too slow":
- An API or database call is taking longer than the limit
- The function does not return a response on some code path
- An infinite loop
- An upstream service is failing
- An unhandled exception
The second and fourth cases have a cheap fix: put your own, shorter timeout on outgoing calls, so a stuck dependency becomes a fast, readable error instead of a five-minute hang ending in 504.
export async function GET() {
try {
const res = await fetch("https://api.example.com/rates", {
signal: AbortSignal.timeout(8000),
});
if (!res.ok) {
return Response.json({ error: "upstream error" }, { status: 502 });
}
return Response.json(await res.json());
} catch {
return Response.json({ error: "upstream timeout" }, { status: 504 });
}
}Now the user sees a failure in eight seconds, your logs say which dependency was slow, and the function is not holding an instance open for nothing. Raising maxDuration in this situation would only make users wait longer for the same failure.
When the work does not belong in the request
If the work is legitimately long — generating a large export, processing an upload, calling a slow third-party API many times — there are three patterns, depending on whether the user needs the result right now.
The user does not need the result: after()
Logging, analytics, cache warming, sending a notification email: things the response does not depend on. Next.js has after() for this. It runs a callback after the response has been sent, and on Vercel it is backed by waitUntil, which keeps the invocation alive until the promise settles.
import { after } from "next/server";
export async function POST(request: Request) {
const order = await saveOrder(await request.json());
after(async () => {
await sendConfirmationEmail(order.id);
});
return Response.json({ id: order.id }, { status: 201 });
}The user gets the 201 immediately. But note what the Next.js docs say about duration: after runs for the platform's default or configured max duration of the route. It moves work out of the user's way; it does not escape the timeout. A ten-minute job inside after() on Hobby still gets killed at five minutes.
The user needs a result, eventually: hand it to a queue
For work that takes minutes, the reliable answer is the one that predates serverless: accept the request, put a job on a queue, return 202 Accepted with an id, and let a worker that is not bound by a request timeout do the work. The client polls a status endpoint or gets notified when it finishes.
If you already run a Django backend next to Next.js, the worker is probably Celery, and the Next.js side only has to forward the request:
# views.py (Django)
from django.http import JsonResponse
from .tasks import generate_export
def start_export(request):
task = generate_export.delay(request.user.id)
return JsonResponse({"task_id": task.id}, status=202)The Next.js function now does a few milliseconds of work per request, and the long part runs on a machine where five minutes is not a wall. I wrote up the Celery side, including retries and why the task should take an id rather than an object, in Django, Celery and Redis: Background Tasks Done Right.
If you want to stay entirely on Vercel, their documentation points to Vercel Workflows for work that needs to pause and resume beyond a single function's limit. I have not used it in production, so I will not describe it beyond that.
The user is waiting and the result arrives in pieces: stream it
For AI responses or long lists, streaming gets the first bytes to the user quickly, which matters a lot on the Edge runtime with its 25-second first-byte rule, and it keeps idle connections from being dropped by proxies. It does not extend the limit. As noted above, streaming time counts toward max duration.
A word on cost
On Pro, functions are billed on active CPU time plus provisioned memory time. Time spent waiting on I/O (a database query, an external API) does not count as active CPU, but the instance is still running, so memory time accrues. A higher maxDuration is not free money for runaway functions; Vercel's own advice is to set a value that covers normal operation and stops abnormal ones. Keep the default unless one specific route needs more, and set it on that route only.
Summary
FUNCTION_INVOCATION_TIMEOUTis a 504 that means the invocation hit its max duration. Streaming time counts.- On Hobby the default and the maximum are both 300 seconds. There is nothing to raise.
- On Pro, set
export const maxDurationin the route or page that needs it (Server Actions use the page's value), or usevercel.jsonwith asrc/-prefixed glob if you have asrcdirectory. Code beatsvercel.json, which beats the dashboard. - Before raising anything, check for a missing response or a stuck upstream call, and put your own short timeout on outgoing requests.
- Long work that the user does not wait for goes into
after(), as long as it fits the limit. Anything longer goes to a queue and a worker.