Next.js 'TypeError: fetch failed' (ECONNREFUSED): Fix

Server-side fetch throws 'TypeError: fetch failed' with ECONNREFUSED in Next.js? Read the real error in err.cause, then fix build-time fetches, localhost IPv6 and hardcoded URLs.

TypeError: fetch failed is a wrapper. The real reason is in error.cause, and for ECONNREFUSED it almost always means your server-side code tried to reach an address where nothing was listening. The usual culprits are a fetch to your own API route during next build, a localhost URL that does not exist in production, or localhost resolving to IPv6 when your service listens on IPv4. Log the cause first, then match it to one of the fixes below.

Many teams hit this right after a deploy. The page worked in next dev, then the build or the live server throws. The fix is usually one line once you know which case you are in.

Step 1: Read the real error in error.cause

Node's built-in fetch throws a generic TypeError: fetch failed. The useful part is hidden one level down.

try {
  const res = await fetch(url);
  return await res.json();
} catch (err) {
  const e = err as Error & { cause?: { code?: string; message?: string } };
  console.error("fetch failed", {
    url,
    message: e.message,
    code: e.cause?.code,
    cause: e.cause?.message,
  });
  throw err;
}

You will see values like these:

  • ECONNREFUSED 127.0.0.1:3000: nothing is listening on that host and port.
  • ECONNREFUSED ::1:1337: the request went to the IPv6 loopback address.
  • ENOTFOUND: the hostname does not resolve.
  • UND_ERR_CONNECT_TIMEOUT: the host did not answer in time.
  • self-signed certificate or unable to verify the first certificate: a TLS problem.

Log the URL as well. Half of these bugs are a URL that is simply not what you think it is.

Step 2: Check if the fetch runs at build time

The most common cause in the App Router is a server component that calls your own route handler, like fetch("http://localhost:3000/api/products"). In next dev the server is running, so it works. During next build Next.js renders static pages, and no server is listening on port 3000 yet.

A Next.js maintainer answered this in the TypeError: fetch failed issue on GitHub: fetching internal API routes at build time is not supported, because there is no API server running during the build. The thread was closed as by design.

You can confirm it fast. If the error appears in the build log and the stack shows a page being generated, this is your case.

Step 3: Stop calling your own API from a server component

A server component already runs on the server. It does not need an HTTP hop to reach your own code. Move the logic into a function and call it from both places.

// lib/products.ts
import { db } from "@/lib/db";

export async function getProducts() {
  return db.product.findMany({ take: 50 });
}
// app/products/page.tsx
import { getProducts } from "@/lib/products";

export default async function Page() {
  const products = await getProducts();
  return <ProductList products={products} />;
}
// app/api/products/route.ts
import { getProducts } from "@/lib/products";

export async function GET() {
  return Response.json(await getProducts());
}

This removes the network call, the build-time failure and the extra latency. If you cannot refactor yet, a temporary option is export const dynamic = "force-dynamic" on the page, so it renders at request time. That hides the build error but turns off static generation for that page, so treat it as a stopgap.

Step 4: Replace hardcoded localhost URLs

If you must call an HTTP endpoint, never hardcode localhost. In production, localhost points at the function or container itself, not at your API.

const base = process.env.API_BASE_URL; // https://api.example.com
if (!base) throw new Error("API_BASE_URL is not set");

const res = await fetch(`${base}/products`);

Set API_BASE_URL in each environment, not only locally. A missing variable gives you undefined/products or a fallback to localhost. Our guide on environment variables that are undefined in production covers the scope and redeploy traps. On Vercel, the VERCEL_URL variable holds the deployment hostname without https://, so add the protocol yourself. Note that it points at the specific deployment, which may be protected by authentication on preview deployments.

Step 5: Fix localhost resolving to IPv6

If the cause says ECONNREFUSED ::1:<port> and your backend is a local service, the two sides disagree on address family. Your service listens on 127.0.0.1 only, but localhost resolved to ::1. People reported this on Node 17 and 18 in the same GitHub issue, and several fixed it by switching the URL to 127.0.0.1.

Two fixes:

// Option 1: use the IPv4 address in the URL
const res = await fetch("http://127.0.0.1:1337/api/posts");
// Option 2: next.config.js, prefer IPv4 when resolving names
const dns = require("node:dns");
dns.setDefaultResultOrder("ipv4first");

module.exports = {};

This is only a local development problem in most setups. In production your upstream should have a real hostname. Results were mixed in the thread, so if the change does nothing for you, go back to Step 2, because the real problem may be that nothing is listening at all.

Step 6: Check TLS, DNS and timeouts

If the cause is not ECONNREFUSED, the same wrapper hides other failures.

  • Self-signed certificate: Fix the certificate chain, or add your CA with NODE_EXTRA_CA_CERTS. The thread mentions NODE_TLS_REJECT_UNAUTHORIZED=0, but that disables certificate checks, so keep it to local development.
  • ENOTFOUND: Check for a typo in the hostname and whether the host is reachable from where your code runs. A private hostname that works on your laptop VPN will not resolve from Vercel.
  • Timeouts: Add a deadline so a slow upstream does not hang the whole page. Our post on Node.js API timeouts and retries shows a pattern with AbortController.
  • Connection reset mid-request: That is a different error with its own cause. See Node.js ECONNRESET.

Step 7: Handle failure so one bad fetch does not break the page

Wrapping a fetch in try/catch does not fix the cause, and the thread says as much. It does stop one failed call from taking down the whole route. Return a safe fallback, log the cause, and make the failure visible.

export async function getRecommendations() {
  try {
    const res = await fetch(`${process.env.API_BASE_URL}/recommendations`, {
      signal: AbortSignal.timeout(3000),
    });
    if (!res.ok) throw new Error(`Upstream returned ${res.status}`);
    return await res.json();
  } catch (err) {
    console.error("recommendations failed", err);
    return [];
  }
}

Only do this for optional data. For a checkout or login call, fail loudly and show the user a real error. For more on turning these into useful 500 pages, see 500 errors in Next.js.

How Relia Helps

A build-time failure shows up in your build log. A runtime fetch failed is harder, because users only see a broken page and the cause sits in a log line. Relia watches users, APIs, the database and servers of a live product, and joins related signals into one incident explained in plain words, such as a deploy followed by failing requests and users hitting an error. It shows who was affected, and when there is a stack trace it gives a fix plan with the file and line. After you deploy a fix, it checks whether the fix worked. For Node.js and browser apps, setup is npx relia-sdk@latest init. There is a Free plan, and you can try the live demo.

FAQ

Why does fetch work in next dev but fail in next build?

In dev, your server is running, so a call to localhost:3000 succeeds. During the build no server is listening, so the connection is refused. Call the data source directly instead of your own API route.

Is "TypeError: fetch failed" a Next.js bug?

Usually not. It is Node's fetch reporting that the connection failed. Read error.cause to see the real reason. The maintainers closed the main GitHub issue as not planned and described the build-time case as by design.

Should I use 127.0.0.1 instead of localhost?

It helps when the cause shows ::1 and your service only listens on IPv4. Reports in the GitHub thread were mixed, so check the cause first. It will not help if nothing is running on that port.

Can I set force-dynamic to make the error go away?

It works because the page stops rendering at build time. You lose static generation for that page, and the fetch can still fail at runtime, so fix the URL or call the data directly.

Why does it only fail after deploying to Vercel?

Most often a hardcoded localhost URL or a missing environment variable. Set the base URL for the Production environment and redeploy.

Sources: vercel/next.js issue #44062, vercel/next.js issue #49578.

All guides