Home › JavaScript › ERR_CONNECTION_REFUSED: Fix It in 5 Minutes
Beginner 6 min · September 23, 2026

ERR_CONNECTION_REFUSED: Fix It in 5 Minutes

Start the server and match the port, then retry — most ERR_CONNECTION_REFUSED cases come from nothing listening or a wrong port, and take minutes to fix..

N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
✓ Production
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
Before you start⏱ 8 min
  • ✓A local dev server you can start and stop (Node, Vite, or Express)
  • ✓A terminal where you can run curl against localhost
  • ✓Basic comfort reading browser DevTools Network panel
 ● Production Incident 🔎 Debug Guide
⚡Quick Answer
  • The server isn't running or it crashed — start it and wait for the listening log line before reloading the browser
  • The page calls the wrong port or host — diff the fetch URL against the server's listen line character by character
  • localhost, 127.0.0.1, and 0.0.0.0 aren't the same — 0.0.0.0 is listen-only and never works as a browser address
  • Prove it with curl: curl failing too means a server fault, curl working means the bug is in your page
✦ Definition~90s read
What is ERR CONNECTION REFUSED Fix?

At the socket level, ERR_CONNECTION_REFUSED is Chromium's name for a TCP connection that earned an RST: your machine sent SYN to an IP and port, and the other end replied 'nothing here'. This is distinct from ERR_CONNECTION_TIMED_OUT (your SYNs vanished into silence — the host may be down or filtered) and ERR_NAME_NOT_RESOLVED (DNS never produced an IP).

★
Imagine calling a shop's phone number and hearing 'this line is disconnected' instead of ringing.

Refused is oddly good news: the network path works, the host is up, and no firewall ate your packets. Something just isn't accepting on that exact IP-plus-port pair.

Firefox says it differently — 'Unable to connect' — and curl says 'Failed to connect: Connection refused', but all three describe the same RST. The usual triggers cluster tightly. The server process isn't running or crashed at boot, so the OS has no socket in LISTEN state.

The server is up but on another port, and you're knocking next door. The server bound a specific interface (127.0.0.1) while you dialed another (a LAN IP, or ::1 via localhost). Or a container boundary moved the meaning of localhost under you. Notice what's absent: your fetch syntax, your headers, your CORS config — none of those can produce a refusal.

Transport fails before HTTP begins.

That ordering dictates the debugging order. Confirm a listener exists (server log, lsof -i :PORT, curl -v), then confirm you're dialing its exact address, then — only then — look at page code. Developers who invert this spend an hour in the wrong layer because every layer's symptom is the same red line.

Read the error literally: connection refused, so fix the connection — process, port, host — and the page heals itself.

Plain-English First

Imagine calling a shop's phone number and hearing 'this line is disconnected' instead of ringing. The phone network works fine — there's just nobody on the other end. ERR_CONNECTION_REFUSED is the same: your browser dialed the right computer but the specific extension number (the port) had no one answering. Either the shop is closed (server stopped), you dialed the wrong extension (wrong port), or you called the shop's internal line from outside (localhost inside Docker).

You're staring at a blank app and a red console line: ERR_CONNECTION_REFUSED on every request. The UI code looks right, the fetch URL looks right, and yesterday everything worked. Most developers start rewriting components at this point — new fetch wrappers, cleared storage, a different browser. None of it helps, because the page was never the problem.

This error is the browser telling you something precise: it reached the machine, knocked on the port, and nobody answered. That narrows the suspect list to three things — a server that isn't running, a port or host that doesn't match, or a container boundary that moved loopback under your feet. Each has a 30-second check, and this guide gives you all of them.

You'll learn to read the error literally, tell localhost apart from 127.0.0.1 and 0.0.0.0, survive Docker's per-container loopback, handle https-to-http quirks, and use curl to split every case into server fault or page fault in one command. Keep this page open during your next red console and work it top to bottom.

Nothing Is Listening: The Server Isn't Running or Crashed

Refused is the most literal error in the browser: your TCP SYN packet reached the host, and the host replied RST — no queue, no backlog, nothing. Contrast that with a timeout (packets vanish, cause unknown) or a DNS failure (you never found the host). RST means the road works and the door is shut. So the first question is never 'is my code right?' but 'is any process holding that port open right now?' That check takes seconds: look at the server terminal for a listening line, or run curl -v and watch for an instant refusal instead of a hang.

A crashed-at-boot server is the classic. Nodemon or ts-node restarts, the app throws on an import, the process exits, and the port goes quiet — while the browser tab keeps retrying the old address. You'll see refused on every request with zero server logs, because there is no server. Scroll the terminal up: the real error (a bad import, a missing env var, a port clash) is sitting above the last restart line. Fix that, watch the process stay up for ten seconds, and only then reload the tab.

The second classic is simpler: the server was never started. Frontend and backend live in different terminals, the machine rebooted, or you opened the project straight into the UI folder. Make startup a single command (npm run dev with concurrently, or Compose up for both sides) so one action can't leave half the system down. And give the API a /health route — a 200 from /health is the cheapest proof of listening you'll ever own, and your deploy pipeline can curl it too.

health-check.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// health.js — tiny probe that answers the 'is anything listening?' question
async function isServerUp(baseUrl) {
  const ctrl = new AbortController();
  const timer = setTimeout(() => ctrl.abort(), 3000);
  try {
    const res = await fetch(baseUrl + '/health', { signal: ctrl.signal });
    return res.ok;
  } catch (err) {
    // TypeError: Failed to fetch covers refused, DNS, and TLS faults
    console.error('No listener at ' + baseUrl + ':', err.message);
    return false;
  } finally {
    clearTimeout(timer);
  }
}

isServerUp('http://localhost:3000').then((up) => {
  console.log(up ? 'server is listening' : 'nothing listening — start the server');
});
Try it live
📊 Production Insight
A boot crash that exits in 200ms leaves the port shut and the console blaming the network. The true error sits in the server terminal above the restart line — always scroll up first.
🎯 Key Takeaway
RST means the host is reachable but the port is shut — verify a live listener with the server log or curl before touching page code.

Wrong Port or Host: The Typo That Wastes an Hour

Once a listener exists somewhere, the next suspect is the address you knocked on. Ports are exact numbers with no fuzzy matching: :3000 and :3001 are as different as two cities. Frontend env vars (VITE_API_URL, REACT_APP_API_URL, API_BASE_URL) go stale silently — a backend move from 3000 to 4000, a second project grabbing 3000 first, or a README example you copied with a different port. The symptom is maddeningly stable: every request refused, server healthy, because you're calling an empty port two digits away from the live one.

Build a habit that kills this class forever: log the full base URL when the frontend boots, and read the server's listen line when it starts. Put the two side by side — if they differ, you've found it in seconds. Centralize the value so there's exactly one place to update: one env var, one config module, every fetch built from it. Never scatter literal 'http://localhost:3000' strings across ten files, because the eleventh file you forgot is the one that pages you.

Watch for the port-squat variant too: something else already owns your port, so your server either crashed with EADDRINUSE or quietly bound elsewhere. If the server log shows an unexpected port, run lsof -i :3000 (macOS/Linux) or netstat -ano | findstr :3000 (Windows) to name the squatter. Kill the stale process or move your server deliberately — but never leave two services fighting over one port and guess which one won.

api-client.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// api-client.js — one base URL, validated once, used everywhere
const API_BASE = (process.env.API_BASE_URL || 'http://localhost:3000').replace(/\/$/, '');

if (!/^http:\/\/localhost:\d+$/.test(API_BASE) && !/^http:\/\/127\.0\.0\.1:\d+$/.test(API_BASE)) {
  console.warn('API_BASE looks unusual: ' + API_BASE);
}
console.log('API base: ' + API_BASE);

export async function getStatus() {
  const res = await fetch(API_BASE + '/health');
  if (!res.ok) {
    throw new Error('API answered ' + res.status + ' at ' + API_BASE);
  }
  return res.json();
}
Try it live
📊 Production Insight
Stale VITE_ vars survive server restarts because Vite bakes them at boot. After changing any API URL var, restart the dev server — a plain page reload keeps the old value.
🎯 Key Takeaway
One env var owns the base URL, the boot log prints it, and it must match the server's listen line digit for digit.

localhost vs 127.0.0.1 vs 0.0.0.0: What Each One Means

Three strings that look interchangeable will ruin your afternoon if you treat them as identical. localhost is a hostname that usually resolves to 127.0.0.1 (IPv4) or ::1 (IPv6) — whichever your resolver prefers that day. 127.0.0.1 is the IPv4 loopback address, always IPv4, no DNS involved. 0.0.0.0 is not a destination at all: it's the wildcard a server binds to mean 'accept on every local interface'. Each has one correct position — dial versus listen — and swapping them breaks things in ways that look haunted.

The IPv4/IPv6 split is the haunting. Your Node server listens on 127.0.0.1 (IPv4 only), but the browser resolves localhost to ::1 (IPv6) and knocks there — refused, while 127.0.0.1 in the address bar works fine. Or the reverse after an OS update flips resolution order. When localhost fails but 127.0.0.1 succeeds, you've found it: either bind the server to both families (listen without a host argument) or standardize the URL on 127.0.0.1. Don't reorder resolver config to fix one app.

Then there's the position error: pasting 0.0.0.0 into the browser because the server log said 'listening on 0.0.0.0:3000'. That log line describes the bind, not a dialable address — translate it to localhost on the same machine. Going the other way, binding to 127.0.0.1 and expecting your phone on the same Wi-Fi to connect will refuse every time: loopback never leaves the machine. For LAN testing, bind 0.0.0.0 and dial the machine's LAN IP (192.168.x.x), keeping the firewall in mind on public networks.

addrs.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// addrs.js — know which dial host matches your bind
// Server bound to 127.0.0.1  -> reach it at localhost or 127.0.0.1 only
// Server bound to 0.0.0.0      -> reach it at localhost, 127.0.0.1, or <lan-ip>
function dialUrl(bindHost, port) {
  if (bindHost === '0.0.0.0') {
    return 'http://localhost:' + port; // same machine
  }
  return 'http://' + bindHost + ':' + port;
}

console.log(dialUrl('0.0.0.0', 3000)); // http://localhost:3000
console.log(dialUrl('127.0.0.1', 3000)); // http://127.0.0.1:3000

// From a phone on the same Wi-Fi, use the LAN IP instead:
// http://192.168.1.20:3000  (server must bind 0.0.0.0 for this)
Try it live
🔥Read 0.0.0.0 by Position
0.0.0.0 in a server log is good news — it means all interfaces. 0.0.0.0 in a browser URL is always wrong. Translate it to localhost on the same machine or the LAN IP from another device.
📊 Production Insight
An OS update flipped localhost to ::1 on half the team's laptops while the API bound IPv4-only. Standardizing dev URLs on 127.0.0.1 ended a week of 'works on my machine' tickets.
🎯 Key Takeaway
localhost is a name, 127.0.0.1 is IPv4 loopback, 0.0.0.0 is listen-only — match the dial host to the interface the server bound.

Docker Loopback Trap: Why localhost Fails in Containers

Docker gives every container its own loopback interface, and that breaks every localhost assumption your code grew up with. Code that fetches http://localhost:3000 works on your laptop because app and API share one loopback. Move the caller into a container and localhost now means the caller's own container — the API isn't there, so the connection is refused instantly with no server-side trace. The API logs show nothing because the request never left the wrong house.

The fix is naming: inside Compose, services reach each other by service name over the shared network — http://api:3000 when the service is called api. Only the host uses localhost, and only through a published port (ports: ['3000:3000']). Keep the two call sites straight: browser-on-host talks to localhost:3000, containerized worker talks to http://api:3000. An env switch like RUNNING_IN_DOCKER picks the right base per runtime so one codebase serves both.

Two companion traps ride along. First, a server inside a container that binds 127.0.0.1 is reachable only within its own container — bind 0.0.0.0 inside Docker so the published port can forward in. Second, port publishing itself: without ports: in Compose (or -p 3000:3000 in plain Docker), the host's localhost:3000 has nothing to forward to. When host-side calls refuse but container-to-container works, check the publish mapping with docker ps before anything else.

compose-url.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// compose-url.js — pick the base URL by where the caller runs
// docker-compose.yml: services 'web' and 'api'; api exposes 3000
function apiBase() {
  // Inside a container, the API is a sibling service, not localhost.
  if (process.env.RUNNING_IN_DOCKER === '1') {
    return 'http://api:3000';
  }
  return 'http://localhost:3000';
}

async function fetchUsers() {
  const base = apiBase();
  console.log('calling ' + base + '/users');
  const res = await fetch(base + '/users');
  if (!res.ok) {
    throw new Error('GET /users -> ' + res.status);
  }
  return res.json();
}
Try it live
📊 Production Insight
Container-to-container checks passed while host calls refused — the Compose file was missing its ports: entry. docker ps showed no mapping, and one line of YAML ended the incident.
🎯 Key Takeaway
Between containers use the service name, from the host use localhost plus a published port, and bind 0.0.0.0 inside the image.

https-to-http and Browser vs curl: Isolating the Layer

Two failures wear the refused costume. The first is scheme mismatch: an https page fetching http://localhost gets blocked as mixed content. Chromium blocks it outright; the console message talks about insecure requests while your API sits healthy on plain http. It works when you open the page over http, which sends you chasing ghosts. The durable fix is one scheme everywhere in dev — proxy /api through the dev server so the browser sees a single origin, or serve local https on both sides with mkcert.

The second is blaming page code for a transport failure. Extensions (ad blockers, privacy tools), proxy env vars, and service workers can each break or reroute a request the server would have answered. curl cuts through all of it: curl -v http://localhost:3000/health speaks raw HTTP with no extensions, no worker, no cached DNS. If curl succeeds while the page fails, work the page side — disable extensions in a fresh profile, unregister the service worker, check proxy bypass for localhost. If curl fails, stay on the server side.

Make the split a reflex and in this order: server log, curl, then browser. The log proves a listener, curl proves reachability without browser interference, and only then does DevTools earn your attention. Teams that run this order resolve refused cases in minutes; teams that start in the Components panel donate afternoons. The browser's Network tab is step three, never step one.

vite.config.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// vite.config.js — one origin in dev, no scheme mismatch, no CORS preflights
// The page calls /api/users; Vite forwards to http://localhost:4000.
module.exports = {
  server: {
    proxy: {
      '/api': {
        target: 'http://localhost:4000',
        changeOrigin: true
      }
    }
  }
};

// page code stays origin-clean:
// const res = await fetch('/api/users');
Try it live
📊 Production Insight
An ad blocker silently killed localhost analytics calls while the app's own fetches worked, faking a partial outage. A clean-profile retest with extensions off is now step two of the runbook.
🎯 Key Takeaway
curl decides the side: curl fails means server fault, curl works means page fault — unify the scheme and proxy /api to one origin.

The 5-Minute Fix Checklist That Ends Refused Errors

When the console turns red, run this sequence and stop at the first failure. One: read the server terminal — is the process alive and does it print a listening line with the port you expect? If it exited, fix the boot error above the last restart. Two: curl -v your health endpoint with the exact host and port from that log line. Instant refused means server-side; success means page-side. Three: diff the page's base URL against the log line digit by digit, then restart the dev server so fresh env vars load — Vite and CRA bake them at boot.

Four: check the host position. Browser URL with 0.0.0.0 is always wrong — swap to localhost. localhost failing while 127.0.0.1 works means an IPv4/IPv6 split — standardize on 127.0.0.1 or bind both families. In Docker, container callers use the service name and the image binds 0.0.0.0 with a published port. Five: compare schemes — https page plus http API means proxy /api through the dev server. Six: retest in a clean profile with extensions off and the service worker unregistered.

Write down what you found, even if it's 'server wasn't started'. The note feels silly once, then saves the next person an hour. Better: log the API base URL at frontend boot and expose /health on every backend, so the next refusal announces its own cause in the first two lines of output.

📊 Production Insight
Teams that log the base URL at boot and curl /health in the deploy pipeline turn 40-minute mysteries into 2-minute checks — the evidence is printed before anyone asks.
🎯 Key Takeaway
Log, curl, diff the port, fix the host position, unify the scheme, retest clean — the first failing step names the culprit.
● Production incidentPOST-MORTEMseverity: high

The One-Port Deploy Move That Broke Every Dashboard for 38 Minutes

Symptom
Right after a routine deploy, the dashboard loaded its shell but every data panel spun forever. The console showed ERR_CONNECTION_REFUSED for all /api calls. The API's own health endpoint answered fine when curled directly on the new port, which confused the team into suspecting the CDN for 20 minutes.
Assumption
The team assumed the API hadn't changed, because no backend code shipped that day. The frontend env still pointed at :3000, which had been correct for months. Nobody re-checked the port after the infrastructure change, since ports felt like settled config.
Root cause
The API's Compose file changed its published port from 3000 to 4000 to dodge a clash with another service. The frontend's API URL env var still said :3000. Every browser request knocked on a port with nothing behind it, so each got a TCP RST — ERR_CONNECTION_REFUSED. The backend was healthy on :4000 the whole time; curl against :4000 returned 200 while the incident was ongoing.
Fix
They set the API base URL from a single env var, logged it on frontend boot, and added a /health check the deploy pipeline curls before marking the deploy green. The pipeline now fails fast when the API isn't reachable, instead of shipping a frontend that can't call home. They also pinned the Compose port mapping next to the env example so the two stay in sync.
Key lesson
  • Log the full API base URL on frontend startup — a wrong port then shows up in the first log line, not after an hour of component debugging.
  • Gate deploys on a health check the pipeline curls itself; a deploy that can't reach its API should fail before users see it.
  • Keep port mappings and frontend env examples side by side so an infra change forces an env review in the same diff.
Production debug guideFive checks that split every refused connection into server fault or page fault.5 entries
Symptom · 01
Browser shows ERR_CONNECTION_REFUSED on every API call
→
Fix
Look at the server terminal for a listening line (listening on 3000). If the process exited, scroll up for the crash — a thrown error at boot is the most common cause. Restart it, watch it stay up for 10 seconds, then reload. If there's no terminal, check ps aux | grep node or your process manager before touching any code.
Symptom · 02
You can't tell whether the server or the page is broken
→
Fix
Run curl -v http://localhost:3000/health (swap in your real port and path). Instant 'Connection refused' means the server side is at fault — keep debugging there. A curl success with JSON means the server is healthy and the bug is in the page: wrong URL string, stale env var, or a service worker serving an old bundle.
Symptom · 03
Server is up but the page still gets refused
→
Fix
Read the server's actual listen line — port and host — and diff it against the page's URL character by character. Fix the page's env var (VITE_API_URL, REACT_APP_API_URL) to match, restart the dev server so the var reloads, and hard-reload the page. Log the base URL at startup so the next mismatch announces itself.
Symptom · 04
Works on the laptop, refused inside Docker Compose
→
Fix
From inside the container, localhost is the container itself. Exec in (docker exec -it <app> sh) and test with the service name: wget -qO- http://api:3000/health. If that works, change the app's base URL to http://api:3000. From the host, use localhost with the published port and verify the mapping with docker ps.
Symptom · 05
Fails on the https origin but works over plain http
→
Fix
Open DevTools → Network, click the failed request, and read the scheme: an https page fetching http:// triggers mixed-content blocking that looks like a network failure. Unify the scheme — proxy /api through the dev server (server.proxy) so the browser sees one origin — or serve local https via mkcert on both sides.
ERR_CONNECTION_REFUSED Causes at a Glance
Root CauseHow to ConfirmFixPrevention
Nothing listening — server stopped or crashedcurl -v fails instantly; server terminal shows no 'listening' line or an exited processStart the server, fix the crash, confirm the listening log, then reloadAdd a /health endpoint and a startup script that blocks the frontend until the API answers
Wrong port in the browser URL or fetch callServer log says :4000 but the page calls :3000; curl on the logged port succeedsCorrect the URL to the logged port; centralize the port in one env varRead the port from a single shared config; log the full base URL at startup
Wrong host — 0.0.0.0 in the browser, or localhost inside DockerURL contains 0.0.0.0, or code runs in a container calling localhost for another serviceUse 127.0.0.1/localhost from the host; use the Compose service name between containersBind explicitly (127.0.0.1 vs 0.0.0.0) and document which host each caller must use
Scheme mismatch — https page calling http APIWorks over plain http, fails on the https origin; console cites insecure or blocked requestServe both sides over the same scheme or proxy the API through the dev serverProxy /api through Vite/webpack dev server so the browser sees a single origin
⚙ Quick Reference
5 commands from this guide
FileCommand / CodePurpose
health-check.jsasync function isServerUp(baseUrl) {Nothing Is Listening
api-client.jsconst API_BASE = (process.env.API_BASE_URL || 'http://localhost:3000').replace(/...Wrong Port or Host
addrs.jsfunction dialUrl(bindHost, port) {localhost vs 127.0.0.1 vs 0.0.0.0
compose-url.jsfunction apiBase() {Docker Loopback Trap
vite.config.jsmodule.exports = {https-to-http and Browser vs curl

Key takeaways

1
Refused means the host answered but nothing listens on that port
check the server process first, not your components.
2
The port in the page must match the server's listening log exactly; centralize it in one env var.
3
localhost, 127.0.0.1, and 0.0.0.0 aren't interchangeable
0.0.0.0 is listen-only, never a browser address.
4
Inside Docker each container owns its loopback
use the Compose service name between containers.
5
curl splits every case
curl fails means server fault, curl works means page fault.
6
Keep page and API on one scheme and origin in dev
proxy /api through the dev server.

Common mistakes to avoid

5 patterns
×

Typing the port from memory instead of the server log

Symptom
The server runs fine on :4000 but the app fetches :3000 from a stale example. You restart both sides twice before noticing the digits differ. Every reload fails identically, which feels like a deep bug but is a typo.
Fix
Copy the port from the server's startup log, not from memory. If the log says 'listening on 4000', the browser URL must say :4000. Better yet, read the port from one shared config or env var so the two can't drift apart.
×

Binding to 127.0.0.1 and expecting LAN devices to connect

Symptom
Your laptop loads the app but your phone on the same Wi-Fi gets ERR_CONNECTION_REFUSED. The server is healthy — it's just listening on loopback, which no other machine can reach. You blame the router for an hour.
Fix
Bind dev servers to 127.0.0.1 explicitly when you only need local access, and use 0.0.0.0 only when another device or container must reach it. Keep a firewall on so a 0.0.0.0 bind doesn't expose you on public Wi-Fi.
×

Fetching localhost from inside a Docker container

Symptom
Code works on your laptop but fails inside Docker with connection refused. Each container has its own localhost, so the app is calling itself instead of the API container. Logs show the fetch failing instantly with no server-side trace.
Fix
In Docker, call the service by its Compose service name (http://api:3000) from inside containers, and use localhost only from the host through a published port. Print the base URL at startup so a wrong host shows up in the first log line.
×

Loading an https page that fetches http://localhost

Symptom
The fetch fails or gets blocked and the console blames the network, not your code. It works when you open the page over plain http, which makes it look random. The culprit is the scheme mismatch, and it only bites on secure origins.
Fix
Keep the page and the API on the same scheme during development, or serve both over https with a local cert via mkcert. If you must mix, put the API behind the same origin with a dev proxy (Vite server.proxy, webpack devServer.proxy) so the browser sees one origin.
×

Debugging JavaScript before testing with curl

Symptom
You add console logs, rewrite fetch calls, and clear site data for 40 minutes while the server was simply stopped. One curl command in the first minute would have pointed at the dead server and saved the whole session.
Fix
Run curl first for every refused connection: curl -v http://localhost:3000/health. If curl fails, debug the server and network, not your components. If curl succeeds, the bug is in the page — wrong URL string, service worker, or extension.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
What does ERR_CONNECTION_REFUSED mean at the network level?
Q02SENIOR
Why can localhost fail while 127.0.0.1 works, and why does localhost bre...
Q03SENIOR
Why is curl the first diagnostic for a refused connection?
Q04SENIOR
How does container networking change loopback, and how do you fix it?
Q05SENIOR
Why does an https page calling http://localhost fail, and what's the rob...
Q01 of 05JUNIOR

What does ERR_CONNECTION_REFUSED mean at the network level?

ANSWER
It means the browser's TCP SYN got an RST back — the host is reachable but nothing accepts connections on that port. Common causes: the server isn't running, it crashed, it listens on a different port, or it bound to a different interface. Confirm with curl, then check the server's listening log.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
Can I put 0.0.0.0 in the browser address bar?
02
Are localhost and 127.0.0.1 the same thing?
03
Why does localhost fail inside Docker but work outside?
04
Should I debug the frontend or the server first?
05
Is ERR_CONNECTION_REFUSED a firewall problem?
06
My API returns 404 — is that the same fix?
N
Naren Founder & Principal Engineer

20+ years shipping production JavaScript and front-end systems at scale. Lessons pulled from things that broke in production.

Follow
✓ Verified
production tested
September 24, 2026
last updated
1,942
articles · all by Naren
🔥

That's Browser. Mark it forged?

6 min read · try the examples if you haven't

←
Previous
Webpack Module Parse Failed Fix
4 / 7 · Browser
Next
CORS Request Did Not Succeed Fix
→