Fix “TypeError: fetch failed” on Vercel and Next.js
Explore with AI
TypeError: fetch failed Node.js prints “TypeError: fetch failed” when its built-in fetch, which runs on undici, cannot finish an HTTP request: the connection was refused, the host could not be reached, a timeout fired or the socket closed. On Vercel the most common cause is a Server Component or page fetching your own API at http://localhost:3000 during next build, when nothing is listening. Read err.cause to see the real error, then call your data source directly or point the fetch at a URL that is reachable from where the code runs.
Ongoing incident? Our founding team can help you get your Vercel app back up.On this page
- What “fetch failed” means in Node.js
- Cause 1: fetching your own API route on localhost during the build
- Cause 2: a base URL that does not resolve where the code runs
- Cause 3: the upstream is too slow and a timeout fires
- Cause 4: the socket closes partway through the request
- When an unhandled fetch failure becomes a 500
- Keeping fetch failures visible and contained
- Tracing fetch failures on Vercel with Polylane
- Common questions
You see this in a Vercel build log under Error occurred prerendering page, or in runtime logs next to a 500 from a route that calls an API. It often works locally and breaks the first time you deploy. The message is the same whatever went wrong, so on its own it tells you very little.
What it does tell you: a fetch() call in your server code never got an HTTP response. The real reason is one level down, in the error’s cause property. Most of the fix is reading that property.
What “fetch failed” means in Node.js
The Node.js globals docs say the global fetch is based on undici, an HTTP/1.1 client written from scratch for Node.js. You can check which undici version your process bundles by reading process.versions.undici.
When undici fails below the HTTP layer, fetch rejects with a generic TypeError and attaches the low-level error as cause. The undici errors reference says you can read timeout codes from error.cause?.code on wrapped fetch errors. It also tells you to match on code strings, because the global dispatcher may come from a different undici version than one you import, which makes instanceof checks unreliable.
HTTP error statuses such as 404 or 500 come back as a Response with a status code. MDN’s Response.ok page says res.ok is true only for a status from 200 to 299, so check it for those. fetch failed is the case where no status arrived at all: the network step itself failed.
Reading err.cause to find the real error
Log the cause before you change anything:
try {
const res = await fetch(url);
} catch (err) {
console.error("fetch failed", url, err.cause);
throw err;
}
The cause usually falls into one of these groups:
All of these arrive wrapped in TypeError: fetch failed
cause.code: 'ECONNREFUSED' nothing listening at that host and port
cause.code: 'UND_ERR_CONNECT_TIMEOUT' connection not set up before the connect timeout
cause.code: 'UND_ERR_HEADERS_TIMEOUT' response headers did not arrive in time
cause.code: 'UND_ERR_BODY_TIMEOUT' response body stalled between chunks
cause.code: 'UND_ERR_SOCKET' the socket failed or closed mid-request
Then check where the error was printed. If it is in the build output, the fetch ran during next build. If it is in runtime logs, it ran inside a Vercel Function while serving a request. The fix depends on which one.
graph TB A[TypeError: fetch failed] --> B[Log err.cause and the URL] B --> C{Build log or runtime log?} C -->|Build, ECONNREFUSED localhost| D[Call the data source directly] C -->|Runtime| E{What is cause.code?} E -->|ECONNREFUSED or DNS error| F[Fix the base URL per environment] E -->|UND_ERR_*_TIMEOUT| G[Set timeouts and handle them] E -->|UND_ERR_SOCKET| H[Retry idempotent requests] style A fill:#fee2e2,stroke:#fca5a5,color:#7f1d1d style D fill:#d1fae5,stroke:#6ee7b7,color:#065f46 style F fill:#d1fae5,stroke:#6ee7b7,color:#065f46 style G fill:#d1fae5,stroke:#6ee7b7,color:#065f46 style H fill:#d1fae5,stroke:#6ee7b7,color:#065f46
Cause 1: fetching your own API route on localhost during the build
How to tell it’s yours: the build log shows Error occurred prerendering page "/some-route", and the cause reads connect ECONNREFUSED 127.0.0.1:3000. The code does something like fetch("http://localhost:3000/api/event") inside a Server Component, a page or generateStaticParams.
This is the case people hit most. During next build, Next.js prerenders the pages it can render ahead of time. Your app is still being built, so it is not running yet, and unless something else serves port 3000 on that machine the connection is refused. The page throws, and the prerender error fails the build. Locally it often works because next dev is already running on that port while you build.
Vercel’s post on common App Router mistakes opens with this exact pattern. Route Handlers and Server Components both run on the server, so the extra network hop is unneeded, and you can call the logic directly in the Server Component. Server-side fetches also need an absolute URL, which is why people write localhost in the first place.
Fix:
- Move the logic out of the Route Handler into a plain function, and call that function from both the Route Handler and the Server Component:
// lib/events.ts import { db } from "@/lib/db"; export async function getEvents() { return db.event.findMany({ orderBy: { startsAt: "asc" } }); }// app/match/page.tsx import { getEvents } from "@/lib/events"; export default async function Page() { const events = await getEvents(); return <EventList events={events} />; }// app/api/event/route.ts (keep it for browsers and other clients) import { getEvents } from "@/lib/events"; export async function GET() { return Response.json(await getEvents()); } - If the data comes from an external API, fetch that API’s absolute URL straight from the component. Skip your own route.
- Rebuild locally with nothing running on port 3000, so you test the same conditions as Vercel. Next.js has a flag for unminified stack traces with source maps:
next build --debug-prerender - Push and check that the Vercel build log no longer shows the prerender error for that route.
Some answers suggest marking the page dynamic with export const dynamic = 'force-dynamic' so it renders per request and skips the build-time fetch. That moves the problem to runtime, where localhost is still wrong (see Cause 2). It also depends on your caching model: the route segment config docs say Next.js 16 removed dynamic, revalidate and fetchCache when Cache Components is enabled. Calling the function directly works in both models.
Cause 2: a base URL that does not resolve where the code runs
How to tell it’s yours: the error shows up in runtime logs, and the cause is ECONNREFUSED against 127.0.0.1, or a DNS lookup error against a host name. The URL you logged contains localhost, undefined, a staging host that no longer exists, or a host that differs between Preview and Production.
An ECONNREFUSED cause means nothing accepted the connection at that address and port. On Vercel your app is reached through its deployment domain, so a fetch to localhost from a function finds nothing to connect to. A base URL read from an environment variable breaks the same way when that variable is missing in one environment. Vercel scopes variables to Production, Preview or Development, and changes only apply to new deployments, so adding the value does nothing until you redeploy.
Vercel fills in system environment variables that help here, once Enable access to System Environment Variables is ticked under Environment Variables in project settings:
VERCEL_ENVisproduction,previewordevelopment, at build and runtime.VERCEL_URLis the generated deployment domain. It has nohttps://prefix, and the docs say it cannot be used together with Standard Deployment Protection.VERCEL_BRANCH_URLis the generated Git branch domain, also without the protocol.VERCEL_PROJECT_PRODUCTION_URLis the shortest production custom domain, or thevercel.appdomain if there is none. It is always set, even in Preview deployments.
Fix:
- First ask whether the server needs to call itself at all. If the target is your own route, use the Cause 1 fix and call the function.
- If you must call a separate service, build the URL in one helper so every environment gets a real host:
Add the// lib/base-url.ts export function apiBaseUrl(): string { if (process.env.API_URL) return process.env.API_URL; if (process.env.VERCEL_ENV === "production") { return `https://${process.env.VERCEL_PROJECT_PRODUCTION_URL}`; } if (process.env.VERCEL_BRANCH_URL) { return `https://${process.env.VERCEL_BRANCH_URL}`; } return "http://localhost:3000"; }https://yourself, because none of the Vercel URL variables include it. - Check that every variable the helper reads exists in each environment, then redeploy. The vercel env docs list
vercel env ls [environment]andvercel env add [name] [environment]:vercel env ls production vercel env ls preview vercel env add API_URL production vercel --prod - Watch for one trap at build time. A build that fetches your production domain reaches whatever deployment serves that domain right now. A route you add in this commit is not live there yet, so the build can still fail or read stale data.
Cause 3: the upstream is too slow and a timeout fires
How to tell it’s yours: the cause code is UND_ERR_CONNECT_TIMEOUT, UND_ERR_HEADERS_TIMEOUT or UND_ERR_BODY_TIMEOUT. Failures come in bursts when a third-party API or your own backend slows down, and the same request works on retry.
The undici errors reference ties each code to a dispatcher option. connect.timeout covers opening the connection, headersTimeout covers waiting for response headers, and bodyTimeout covers the gap between body chunks.
Fix:
- Set your own deadline per request with
AbortSignal.timeout(), which the Node.js docs describe as a signal that aborts after the given number of milliseconds:
MDN’s AbortSignal.timeout() page says the signal aborts with aconst res = await fetch(`${apiBaseUrl()}/v1/quotes`, { signal: AbortSignal.timeout(5000), });TimeoutErrorDOMException, so checkingerr.name === "TimeoutError"tells your own deadline apart from undici’s timeouts. - To change undici’s timeouts for every fetch, install
undiciand set a global dispatcher. Node.js says this affects both undici and the built-in fetch:// a module every function imports import { Agent, setGlobalDispatcher } from "undici"; setGlobalDispatcher( new Agent({ connect: { timeout: 10_000 }, headersTimeout: 15_000, bodyTimeout: 30_000, }), ); - Keep your fetch deadline shorter than the function’s maximum duration. If the function itself runs out of time, Vercel returns FUNCTION_INVOCATION_TIMEOUT, a 504. See how to debug Vercel function timeouts.
- Handle the failure on purpose: return cached data, a fallback, or a clear error response. Left unhandled, it crashes the route.
Cause 4: the socket closes partway through the request
How to tell it’s yours: the cause code is UND_ERR_SOCKET, often with the text other side closed. It hits a small share of requests to one upstream and does not line up with a deploy.
Undici raises SocketError for a failure on the underlying socket. Its socket property holds the local and remote address and port, the remote family, and the bytes written and read. That tells you which upstream closed on you and how far the request got.
Fix:
- Log the socket details with the cause:
catch (err) { const cause = err.cause; console.error("fetch failed", { url, code: cause?.code, remote: cause?.socket?.remoteAddress, bytesRead: cause?.socket?.bytesRead, }); throw err; } - Retry idempotent requests (GET, or PUT with an idempotency key) a small number of times with a growing delay:
async function fetchWithRetry(url: string, init: RequestInit = {}, tries = 3) { for (let attempt = 1; ; attempt++) { try { return await fetch(url, { ...init, signal: AbortSignal.timeout(5000) }); } catch (err: any) { const code = err?.cause?.code; const retriable = code === "UND_ERR_SOCKET" || code === "ECONNRESET"; if (!retriable || attempt >= tries) throw err; await new Promise((r) => setTimeout(r, 200 * 2 ** attempt)); } } } - Do not blindly retry POSTs that create records. A socket that closed after the server got the body can leave you with a duplicate.
- If one upstream keeps closing connections, raise it with its owner, with the addresses and timestamps from step 1.
When an unhandled fetch failure becomes a 500
A rejected fetch that nothing catches is an unhandled rejection. Inside a Vercel Function that crashes the invocation, and the request returns FUNCTION_INVOCATION_FAILED. If that is the code next to your 500, the fetch is the crash, and the steps in the FUNCTION_INVOCATION_FAILED guide apply too.
To pull the failing requests from production with the vercel logs command:
vercel logs requests --environment production --query 'httpStatus:500' --since 30m
Act fast on older failures: runtime logs are kept for 1 hour on Hobby, 1 day on Pro and 3 days on Enterprise.
Keeping fetch failures visible and contained
- Always log
err.cause. Route every server-side fetch through one wrapper that logs the URL,cause.codeand the socket’s remote address. The next failure then names its own cause. - Fail the build on localhost. Add a CI check that fails if server code contains
localhost:3000:! grep -rn "localhost:3000" app lib --include=*.ts --include=*.tsx - Build with nothing on port 3000. Run
next buildin CI on a clean machine, so a self-fetch fails there before Vercel sees it. - Give every outbound call a deadline. Put
AbortSignal.timeout()on each fetch, sized below the function’s maximum duration. - Alert on 5xx by route. A burst of
UND_ERR_*causes usually means one upstream is struggling, and a per-route error alert shows you which route depends on it.
Tracing fetch failures on Vercel with Polylane
Polylane detects production issues and fixes them by opening pull requests. It connects to Vercel, so when a fetch starts failing it works out why, and the fix arrives ready for your team’s normal review.
Running on Vercel? See how Polylane monitors Vercel in production.
Common questions.
Why does fetch work locally but fail with “fetch failed” on Vercel?
Locally, next dev is usually running on port 3000, so a fetch to http://localhost:3000 during the build finds a server. On Vercel, the build runs before your app is serving anything, so nothing answers on that port and the connection is refused. Call your data function directly from the Server Component, or fetch an absolute URL that exists in that environment.
How do I see the real error behind TypeError: fetch failed?
Read the cause property: console.error(err.cause). Node's fetch runs on undici, which wraps the low-level error, and the undici docs say codes such as UND_ERR_CONNECT_TIMEOUT are found on error.cause?.code. Common values are ECONNREFUSED, UND_ERR_HEADERS_TIMEOUT, UND_ERR_BODY_TIMEOUT and UND_ERR_SOCKET.
Should I use a relative URL like /api/event in a Server Component?
No. Code running on the server with Node.js needs an absolute URL for fetch, as Vercel's App Router guidance points out. For your own routes, skip the HTTP call and import the function the route uses. Both run on the server, so the extra hop gains you nothing.
Can I use VERCEL_URL as the base URL for server-side fetches?
You can, with two caveats from Vercel's docs. The value has no https:// prefix, so add it yourself, and VERCEL_URL cannot be used together with Standard Deployment Protection. For links to production, VERCEL_PROJECT_PRODUCTION_URL is always set, even in Preview deployments.
Does export const dynamic = 'force-dynamic' fix fetch failed?
It stops the fetch from running during next build, so the build passes. But the page then fetches at request time, and the localhost URL is still the wrong address there (see Cause 2). In Next.js 16 with Cache Components enabled, the dynamic option has been removed, so calling your data function directly is the fix that works in both models.
Does a 404 or 500 response from the API cause TypeError: fetch failed?
No. A 404 or 500 comes back as a Response with that status, and MDN says res.ok is true only for statuses from 200 to 299. TypeError: fetch failed means no response arrived at all: the connection was refused, timed out, or the socket closed. Check res.ok separately for HTTP error statuses.
How do I change fetch timeouts in Node.js?
For one request, pass a signal from AbortSignal.timeout() with the number of milliseconds you are willing to wait. For every fetch in the process, install undici and call setGlobalDispatcher with an Agent that sets connect.timeout, headersTimeout and bodyTimeout. Node's docs say the global dispatcher applies to the built-in fetch as well.
Why is my build failing with “Error occurred prerendering page” after fetch failed?
Next.js prerenders pages during next build, and an error thrown while rendering one fails the build. When the thrown error is TypeError: fetch failed, the page fetched something that was unreachable at build time, most often your own app on localhost. Run next build --debug-prerender for unminified stack traces that point at the exact component.
Sources (14)
- Global objects: fetch (Node.js Docs)
- Errors (undici Docs)
- Response: ok property (MDN)
- AbortSignal: timeout() static method (MDN)
- Prerender Error with Next.js (Next.js Docs)
- Route Segment Config (Next.js Docs)
- System environment variables (Vercel Docs)
- Environment variables (Vercel Docs)
- vercel env (Vercel Docs)
- vercel logs (Vercel Docs)
- FUNCTION_INVOCATION_TIMEOUT (Vercel Docs)
- Common mistakes with the Next.js App Router and how to fix them (Vercel)
- Polylane full content
- Polylane documentation
Boris Tane is the founder of Polylane. He previously founded Baselime, observability for the future of the cloud, which Cloudflare acquired. At Cloudflare he built and led the Workers observability team.
Related
- Fix Vercel “FUNCTION_INVOCATION_FAILED” 500 errors Why a Vercel Function returns 500 FUNCTION_INVOCATION_FAILED, how to find the crash in runtime logs, and how to fix throws, missing env vars, files and memory.
- How to debug Vercel function timeouts Debug Vercel FUNCTION_INVOCATION_TIMEOUT 504s: find the slow call with vercel logs and httpstat, fix hangs, set maxDuration and verify on a preview.
- Vercel 504 Gateway Timeout on Serverless Functions: Causes and Fixes Fix Vercel 504 FUNCTION_INVOCATION_TIMEOUT errors: current duration limits, how to find the slow call, the fix for each cause and how to raise maxDuration.
- Fix Vercel “MIDDLEWARE_INVOCATION_FAILED” errors Why Vercel returns 500 MIDDLEWARE_INVOCATION_FAILED or EDGE_FUNCTION_INVOCATION_FAILED, how to find the failing middleware in logs, and how to fix it.
- Fix Prisma “P1001: Can't reach database server” Why Prisma prints “P1001: Can't reach database server”, how to test the host and port it names, and how to fix private hosts, IPv6, paused databases and URLs.