A Netlify Function that runs in 200 ms locally and 300 ms on a warm invocation returns a 502 the first time it is hit after a quiet period. The function log shows Task timed out after 10.00 seconds. You retry within a minute and the same code returns instantly. The function is not slow — it is paying a cold-start tax that exceeds the default 10-second synchronous-function limit on Netlify. The cause is almost always heavy module initialization (a fat SDK, a Prisma client, an OpenAI/Anthropic client doing DNS warm-up), a top-level await against a slow upstream, or a bundle that pulls in aws-sdk v2 transitively.
Fastest fix (works for ~80% of cases): convert top-level import of any heavy SDK into a lazy import() inside the handler (Step 2 below), and run npm ls aws-sdk to confirm no v2 monolith is in the bundle (Step 3). That alone drops cold-start init from 4-6 s to under 1 s. The 10 s ceiling itself is real but not absolute: on the Pro plan ($20/mo as of June 2026, no longer per-seat after Netlify’s April 2026 pricing change) Netlify can raise the synchronous limit to 26 s, and background functions run up to 15 minutes — but raising the ceiling treats the symptom, so fix the init weight first.
Which bucket are you in?
Pick the row that matches your symptom before touching code. Most “timeout” reports are init weight, not slow handler logic.
| Symptom | Most likely cause | Go to |
|---|---|---|
| First call after idle fails, retries are instant | Heavy top-level imports | Step 1, Step 2 |
Bundle (du -sh) is over 10 MB | aws-sdk v2 dragged in | Step 3 |
| Even warm calls take 8-15 s | Wrong runtime (needs background) | Step 4 |
Init has a top-level await fetch(...) | Blocking upstream at module scope | Step 2 |
| Cold call slow, DB/KV in another region | Region mismatch | Step 5 |
Init is genuinely fast (< 1 s) but work is heavy | Raise the limit or go background | Step 4 |
Common causes
Ordered by likelihood for sync Netlify Functions. The default runtime is Node 22 as of June 2026 (Netlify falls back to nodejs22.x unless you pin another via the AWS_LAMBDA_JS_RUNTIME env var, e.g. nodejs20.x).
1. Heavy top-level imports pulled in at cold start
Every import at the top of the file runs during init, before your handler. A single import { PrismaClient } from '@prisma/client' or import OpenAI from 'openai' can add 2-6 seconds to cold start because the SDK eagerly resolves DNS, parses large JSON manifests, or warms up TLS.
How to spot it: Add console.time('init') as the literal first line of the file and console.timeEnd('init') as the last line before exporting the handler. If it logs >= 4 s, init is your problem.
2. Top-level await against a slow or unreachable upstream
A pattern like const config = await fetch(CONFIG_URL).then(r => r.json()) at module scope blocks init until the upstream responds. If that upstream lives in a different region or is down, cold start eats the entire 10 s budget.
How to spot it: Search for await outside any function. Comment them out and redeploy — if cold start drops to under 2 s, this is it.
3. Synchronous limit vs. background functions
Standard Netlify Functions cap at 10 s of total execution including init on Free, Starter, and Personal plans (this is the limit behind the literal Task timed out after 10.00 seconds string). On the Pro plan the synchronous limit can be raised to 26 s (you currently request the bump through the Netlify dashboard / support, as confirmed across Netlify support threads in 2026 — setting timeout in netlify.toml alone is not enough on most accounts). A function that legitimately needs longer belongs in background functions (15-min cap, returns 202) or edge functions (sub-50 ms init, runs on Deno).
How to spot it: The function does real work that takes 8-15 s warm. It is not a cold-start problem — it is the wrong runtime.
4. Bundle inflated by aws-sdk v2 transitive dep
Some older libraries (mailgun-js, certain analytics packages) drag in aws-sdk v2 — a roughly 50 MB monolith that takes 3-5 s to parse at cold start. The Netlify bundler (zip-it-and-ship-it, using esbuild) does not always tree-shake it away.
How to spot it: ls -lah .netlify/functions-internal/<fn>/ and inspect the bundle size, or run du -sh node_modules/aws-sdk. If the bundle is over 10 MB you almost certainly have v2.
5. DNS resolution loop inside an SDK
OpenAI, Anthropic, Stripe, and Twilio SDKs open an HTTPS connection during their first call. If the function’s outbound DNS is slow to resolve api.openai.com or similar (rare, but it happens during certain Netlify region incidents — check the Netlify status page), the first request after cold start can take 4-8 s.
How to spot it: Add console.time('first-api-call') around the first SDK call and log the duration. Compare warm vs. cold.
6. Synchronous filesystem reads of large bundled assets
fs.readFileSync('./prompts.json') or similar at module scope, with a 5+ MB file, takes hundreds of ms on Lambda’s cold cache and can stack with other init costs to blow the budget.
How to spot it: Grep for readFileSync outside the handler. Move large reads inside the handler with a module-level cache.
7. Wrong region for the data upstream
Functions run in US East (cmh, Ohio) by default; if your Postgres / Redis / KV upstream is in EU or Asia, every cold call pays 100-150 ms RTT per query, and init queries multiply that.
How to spot it: Compare your Netlify site’s function region (netlify.toml → [functions.<name>] region) with your DB region. Note Netlify renamed this key from preferred_region to region and switched to airport-style region codes (cmh, iad, dub, fra, sfo, nrt, syd) — see Step 5.
Before you start
- Confirm the failure is cold-start specific: hit the function, wait 15-20 minutes, hit again. If only the first call after idle fails, it is cold start.
- Note the exact Node runtime (the build picks it up from
.nvmrc/NODE_VERSION; the function runtime falls back tonodejs22.xas of June 2026). Startup time differs by a few hundred ms across major versions, which rarely matters next to init weight. - Capture one failing function-log line including the
Task timed out after 10.00 secondsmessage and the request ID. - Know whether the function is a sync function, scheduled function (30 s cap), or background function (15 min cap) — limits differ.
Information to collect
netlify.toml[functions]and[build]sections.- The function file’s import list (top of file).
- Bundle size:
ls -lah .netlify/functions-internal/<fn>/. package.jsondependencies and any peer/transitiveaws-sdk.- Function logs around the failing request ID (init time + handler time).
- Region of the function vs. region of upstream (DB, KV, API).
- Your Netlify plan (Free/Personal cap at 10 s sync; Pro can be raised to 26 s).
Step-by-step fix
Ordered cheapest to most invasive.
Step 1: Time the init phase
Add at the very top of the function file, before any other import if possible:
const __init = Date.now();
import OpenAI from "openai";
// ...other imports
console.log(`[init] imports done in ${Date.now() - __init}ms`);
export const handler = async (event) => {
const __handler = Date.now();
// ...
console.log(`[handler] done in ${Date.now() - __handler}ms`);
};
Trigger a cold start (deploy, wait 20 min, hit). The [init] line tells you if the problem is import weight (>= 3 s) or handler weight.
Step 2: Lazy-import heavy SDKs inside the handler
Replace top-level imports with dynamic imports gated by first use:
let _openai: import("openai").OpenAI | null = null;
async function getOpenAI() {
if (!_openai) {
const { default: OpenAI } = await import("openai");
_openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
}
return _openai;
}
export const handler = async (event) => {
const openai = await getOpenAI();
// ...
};
The handler pays the import cost on its first call, but init time drops from 4-5 s to under 500 ms. Warm calls still pay nothing.
Step 3: Remove aws-sdk v2 from the bundle
Find what pulls it in:
npm ls aws-sdk
Replace the offending dep with a modular v3 client, or pin a lighter alternative:
npm uninstall mailgun-js
npm install mailgun.js form-data
Verify the bundle shrank:
netlify build
du -sh .netlify/functions-internal/<fn>/
Expect a drop from 30-60 MB to under 5 MB and cold start down 2-3 s.
Step 4: Move long work to a background function
If the handler genuinely needs more than the sync limit, move it to a background function (15-min cap). There are two ways to flag one as of June 2026:
The modern way — set background: true in the function’s exported config (Netlify now recommends this over the filename trick):
import type { Config } from "@netlify/functions";
export default async (req: Request) => {
// long-running work here
};
export const config: Config = { background: true, path: "/process-upload" };
The legacy way still works — append -background to the filename:
netlify/functions/process-upload.ts → 10 s cap
netlify/functions/process-upload-background.ts → 15 min cap
Either way the call returns 202 immediately and the function keeps running. Pair it with a status endpoint and a small Netlify Blobs / KV write so the client can poll for the result. See edge function timeout for the parallel Vercel pattern.
Step 5: Pin function region to match your upstream
Netlify renamed the per-function region key from preferred_region to region and now uses airport-style codes (cmh = US East/Ohio, the default; iad = US East/N. Virginia; dub = EU/Ireland; fra = EU/Frankfurt; sfo = US West; nrt = Tokyo; syd = Sydney). In netlify.toml:
[functions]
node_bundler = "esbuild"
[functions."process-upload"]
# Match your primary DB / KV region
region = "iad"
Redeploy. Cold-start handler latency should drop by RTT times the number of init queries. (Per-function region selection requires a paid plan; on free tiers functions stay in the default region.)
Step 6: Cache DNS-warm SDK clients on the module scope
For sync functions where init weight is acceptable but you want warm calls fast, keep the client at module scope BUT skip any work in its constructor:
import OpenAI from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
// No top-level `await openai.models.list()` — that runs at every cold start.
export const handler = async () => {
// First HTTPS call here, not at init.
const r = await openai.chat.completions.create({ /* ... */ });
};
Step 7: Add a scheduled warm-up ping (last resort)
If the function is user-facing and cold-start latency is unacceptable, ping it every 5 minutes from a Netlify scheduled function:
// netlify/functions/warm-up.ts
import type { Config } from "@netlify/functions";
export default async () => {
await fetch(`${process.env.URL}/.netlify/functions/<critical-fn>?warmup=1`);
};
export const config: Config = { schedule: "*/5 * * * *" };
In the target function, short-circuit on event.queryStringParameters?.warmup === "1" so it returns 200 fast without doing real work.
Step 8: Raise the sync limit on Pro (only after init is fixed)
If init is already under 1 s and the handler legitimately needs more than 10 s (a slow third-party API, a heavy DB aggregation), upgrade to the Pro plan ($20/mo as of June 2026, flat org price after Netlify dropped per-seat billing in April 2026) and request the 26 s synchronous timeout through the Netlify dashboard or support. Do not lead with this: a 26 s ceiling still times out under a 12 s cold-start init, and it costs money to mask a bundle problem. Use it only for genuinely slow work that cannot move to a background function.
How to confirm it’s fixed
- Cold-start cycle: deploy, wait 20 min, hit the endpoint. Total response time under 4 s.
- Function log shows
[init] imports done inunder 1500 ms and noTask timed out. - Warm calls (within 60 s of each other) under 500 ms.
- Bundle size on
du -sh .netlify/functions-internal/<fn>/under 10 MB. - Repeat the idle-then-hit test three times; a flaky 1-in-3 timeout means init is still close to the ceiling.
Long-term prevention
- Never put
awaitat module top scope in serverless functions. - Default to lazy
import()for any SDK over 200 kB; keep init dependency-free where possible. - Run
npm ls aws-sdkafter every dependency change; treat any v2 result as a release blocker. - Set a hard rule: functions that legitimately need > 5 s warm execution go to background or edge runtime.
- Add a synthetic cold-start probe in CI: deploy a preview, wait 15 min, hit the endpoint, fail if response > 5 s.
- Keep function logs flowing to a log drain so you can grep for
Task timed outhistorically, not just live.
Common pitfalls
- Assuming the issue is “slow code” and rewriting the handler — when 90% of the time is init.
- Bumping function memory expecting cold start to drop; it helps a little but does not fix a 50 MB bundle.
- Using a background function for a request that needs a synchronous response — the client gets 202 and no result.
- Forgetting that
netlify devruns functions warm in-process, so cold-start bugs only appear in production. - Adding warm-up pings as the primary fix while ignoring a 40 MB bundle — your bill doubles and the symptom returns the moment the warm-up fails.
FAQ
Q: Can I just raise the 10-second limit on a sync Netlify Function?
Sometimes. The default is 10 s on Free, Personal, and Starter plans (that is the limit behind Task timed out after 10.00 seconds). On the Pro plan Netlify can raise the synchronous limit to 26 s — as of June 2026 you request the bump through the dashboard or support, since setting timeout in netlify.toml alone is not honored on most accounts. For anything longer, use background functions (15 min) or move the slow work off the request path. Raising the ceiling does not help if init alone exceeds it.
Q: My function is 2 MB but cold start is still 6 s. Why?
Bundle size is one factor. Top-level await, SDK constructor work, and the TLS handshake to upstream services often dominate. Profile with Date.now() markers around imports vs. handler — most teams discover init time is 80 percent or more of cold start.
Q: Does a newer Node version cold-start faster?
Slightly — typically 100-300 ms faster per major bump on a fresh container. The function runtime defaults to nodejs22.x as of June 2026; you can pin another with the AWS_LAMBDA_JS_RUNTIME env var (set it in the Netlify UI/CLI, not netlify.toml). None of this rescues a 12-second init, so fix init weight first; the runtime version is a rounding error next to that.
Q: Why does it work in netlify dev but time out in production?
netlify dev runs functions warm in a long-lived local process, so the cold-start init cost never happens locally. Cold-start timeouts only appear on a fresh production container after idle. Always reproduce with the deploy-then-idle-then-hit cycle, not netlify dev.
Q: Does the same problem hit Vercel Functions?
Yes, with a different cap. See Vercel build failed and Vercel 500 errors for the equivalent Vercel patterns; the lazy-import and bundle-size fixes apply identically.
Tags: #Troubleshooting #netlify #serverless #cold-start #timeout