Fixing a Netlify Function Timeout on a Slow Model Call
9 min read · updated August 11, 2026
A Netlify function calling a model returns a 502, and the log shows a single line about a task timing out. That line tells you the invocation hit its ceiling. It does not tell you whether the ceiling is the problem — and in most model-call cases it is not.
The error
The response is a 502, and the body is a JSON object with a single errorMessage field. The function log carries the corresponding line from the underlying Lambda runtime, with a request id and a duration:
2026-08-11T09:14:02.118Z 4234f202-e15d-4a25-84ce-79b9c82ac634 Task timed out after 60.01 seconds
Visiting the function URL directly in a browser instead shows Netlify’s "This function has crashed" page. Called with fetch from your frontend there is no page at all, only the 502 in the network tab, which is why this is often first noticed as a generic client-side failure.
The number in that message is your function’s limit, not a constant. Netlify’s functions configuration page documents a 60-second synchronous execution limit, 30 seconds for scheduled functions and 15 minutes for background functions, and states that these are not configurable. Older forum threads quote Task timed out after 10.01 seconds because the synchronous default used to be 10 seconds; if you see a small number, you are reading an old thread rather than a different limit.
Which of the two is it
The critical property of Netlify’s limit — and of every Lambda- derived limit — is that it measures wall-clock time, not CPU time. A function that spends 59 seconds awaiting a provider has consumed 59 seconds of its budget while doing no work at all. So the same 502 covers two very different situations:
- The work genuinely needs more than 60 seconds. A long document summarisation with a large
max_tokens, a reasoning model, or a chain of three sequential calls. Here the ceiling is the problem and no amount of tuning inside the handler fixes it. - One call hung. The provider is rate-limiting you and the connection is open but idle, or a retry loop inside an SDK is quietly making four attempts with backoff, or DNS is slow in the function region. Here the p50 is two seconds and the p99 is a timeout, and raising a limit you cannot raise anyway would not help.
Distinguish them by logging a duration around the outbound call rather than inferring it from the invocation duration, which includes cold start and everything else:
const started = Date.now();
const upstream = await fetch(url, init);
console.log("upstream_ms", Date.now() - started, "status", upstream.status);If upstream_ms clusters just under 60,000 on failures and around 2,000 on successes, it is a hang. If it climbs smoothly with input size, it is real work.
Give the outbound call its own deadline
The single highest-value change is to stop letting the platform ceiling be your only timeout. Node’s fetch imposes no default request timeout, so an idle connection stays open until something else kills it. Own the deadline instead, and leave headroom to return a useful error:
export default async (req: Request) => {
const { prompt } = await req.json();
try {
const upstream = await fetch("https://api.openai.com/v1/responses", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + process.env.OPENAI_API_KEY,
},
body: JSON.stringify({ model: "gpt-4o-mini", input: prompt }),
signal: AbortSignal.timeout(45_000),
});
return Response.json(await upstream.json());
} catch (err) {
if (err instanceof Error && err.name === "TimeoutError") {
return Response.json({ error: "model_timeout" }, { status: 504 });
}
throw err;
}
};A 504 with model_timeout is enormously more useful than a 502 with a crash page: your client can retry it, your monitoring can count it, and nobody has to open the function log to find out what happened. Fifteen seconds of headroom is enough to serialise a response and log.
Making the call fit
If the work is real, shrink it before you move it. In rough order of effect per unit of effort:
- Cap output tokens. Generation is sequential, so duration is close to linear in output length. Halving
max_tokenshalves the dominant term. - Check the SDK’s own retry policy. Several provider SDKs retry a small number of times by default with backoff. Three attempts at twenty seconds each is a timeout built from successful-looking parts. Set the client’s max retries to zero inside a function and retry at the edge of your system instead, where you control the budget.
- Do not chain calls in one invocation. Two sequential model calls in one handler means the timeout is the sum, and a failure in the second discards the first. Split them.
- Watch the payload. Netlify documents a 6 MB buffered request and response limit, with binary payloads base64-encoded — so a large document is both a size problem and a latency problem before the model has done anything.
Why the obvious workarounds do not work
Three things people reach for first, and why each fails here.
- Raising the memory. Netlify does let you configure memory and vCPU — a default of 1024 MB, configurable from 1024 to 4096 MB, or a vCPU value from 0.5 to 2.0, on credit-based Pro and Enterprise plans, with the two settings mutually exclusive. On a platform where CPU and memory are coupled this often buys speed, so the reflex is reasonable. It does not help here, for a mechanical reason: the limit is wall-clock, and the time is being spent waiting on a socket. Doubling the vCPU of a function that is idle does nothing. Memory helps if you are parsing a 6 MB payload; it does not help if you are awaiting a model.
- Upgrading the plan. Netlify documents the 60-second synchronous limit as not configurable, and documents no per-plan variation for it. There is no tier that raises it. This is worth knowing before an upgrade is proposed as the fix, and it is set out in full on the function limits page.
- Retrying from the client. If the cause is real work, a retry is another 60 seconds of provider spend that will fail the same way. If the cause is a hang, a retry may succeed — but a retry of a request whose first attempt is still running upstream can mean you are billed for two completions and use one. Retry only after your own
AbortSignalhas fired, so you know the first attempt is actually abandoned.
There is one more thing the 502 hides that is worth stating plainly: a terminated invocation does not roll anything back. If the handler wrote a row, charged a credit or dispatched a webhook before the model call, that side effect survives the timeout while the caller sees only a failure. Order the handler so the irreversible work happens after the model returns, or key it so a retry cannot duplicate it.
When it cannot fit
There are exactly two documented escapes from the 60-second synchronous limit on Netlify, and they solve different shapes of problem.
Stream. Netlify’s functions API reference documents streaming responses — return a ReadableStream as the body of the Response — with a 60-second execution limit and a 20 MB response size limit. Streaming does not extend the ceiling, but it changes what the user experiences at second three instead of at second fifty-nine, and it means a slow answer is visible rather than indistinguishable from a hang.
Go to the background. A background function runs for up to 15 minutes and returns 202 to the caller immediately. That is a different contract — there is no response body for the client to read — so it fits a job whose result is written somewhere and collected later, not a chat turn. That trade is worked through in the background functions tutorial, and the full set of limits is tabled on the function limits page.