When an uptime check fails, the alert often contains a cryptic code instead of a status: ECONNREFUSED, ETIMEDOUT, ECONNRESET. Many people treat them all as "site down" and start restarting things at random.
They're not the same. Each one points to a different layer, and knowing which is which can save you twenty minutes of guessing at 3am. Let's reproduce all three locally, then talk about what each one usually means in production.
Reproduce them yourself
Save this as errors.js and run node errors.js. It needs nothing but Node.
const net = require("net");
const http = require("http");
function probe(label, opts) {
return new Promise((resolve) => {
const req = http.get({ ...opts, timeout: 2000 }, (res) => {
res.resume();
resolve(`${label}: HTTP ${res.statusCode}`);
});
req.on("timeout", () => req.destroy(Object.assign(new Error("timed out"), { code: "ETIMEDOUT" })));
req.on("error", (e) => resolve(`${label}: ${e.code}`));
});
}
(async () => {
// 1. Nothing listening on this port -> refused
console.log(await probe("refused", { host: "127.0.0.1", port: 8799 }));
// 2. Server accepts the connection but never answers -> our timeout fires
const silent = net.createServer(() => {}).listen(8798);
console.log(await probe("silent", { host: "127.0.0.1", port: 8798 }));
silent.close();
// 3. Server accepts, then slams the connection shut -> reset
const rude = net.createServer((s) => s.destroy()).listen(8797);
console.log(await probe("rude", { host: "127.0.0.1", port: 8797 }));
rude.close();
process.exit(0);
})();
Output on our machine:
refused: ECONNREFUSED
silent: ETIMEDOUT
rude: ECONNRESET
(If something is already listening on port 8799 on your machine, change it. The "refused" case needs a port with nothing on it.)
Three tiny servers, three different failures. Now let's map them to the real world.
ECONNREFUSED: "I reached the machine, and nobody's home"
The connection attempt reached a host, and that host actively said "no": nothing is listening on that port.
What it usually means in production:
- The app process crashed or hasn't started yet (common right after a deploy or reboot).
- The app is listening on a different port or interface than you think (e.g.
127.0.0.1instead of0.0.0.0). - A load balancer or proxy is up, but the backend it forwards to is down. (In that case the proxy usually turns it into a 502 for external users; you'll see
ECONNREFUSEDin the proxy's logs.)
Where to look first: is the process running? Is it listening where you expect?
ss -ltnp | grep 3000 # is anything listening on port 3000?
systemctl status myapp # or your process manager's equivalent
The good news: refused is fast. You get the answer in milliseconds, which also means it's usually a clear, binary problem.
ETIMEDOUT: "I waited and heard nothing"
Either the connection never completed, or it completed and the server never sent a response within your timeout. In our demo it's the second kind: the server accepted the connection and just sat there.
What it usually means in production:
- A firewall or security group silently dropping traffic (no "no", just silence).
- The server is overloaded: accepting connections but too busy to respond.
- The app is stuck: a deadlock, a blocked event loop, every worker waiting on a slow database.
- Network trouble somewhere between your monitor and the server.
Where to look first: can you connect at all, and how long does the server take to answer?
curl -sv --max-time 10 https://example.com/ -o /dev/null
If curl hangs before Connected to, suspect the network or a firewall. If it connects but hangs before the response, suspect the app or something it depends on.
Timeouts are the slowest errors to diagnose, because "nothing happened" has many possible causes. They're also the most likely to be partial: some requests time out, others succeed.
ECONNRESET: "We were talking, and they hung up"
The connection was established, then the other side abruptly closed it (a TCP reset) instead of finishing politely.
What it usually means in production:
- The app crashed mid-request.
- A proxy, load balancer or firewall killed a connection it considered idle or invalid.
- Keep-alive mismatches: your client reused a connection the server had already decided to close. Often intermittent and maddening.
- The server process was restarted during the request (e.g. a deploy without graceful shutdown).
Where to look first: the app's logs and crash reports around the exact timestamp, and any proxy logs in between.
Occasional resets during deploys usually mean you need graceful shutdown: stop accepting new connections, finish in-flight requests, then exit.
A quick cheat sheet
| Error | How fast it fails | Most likely layer | First question |
|---|---|---|---|
| ECONNREFUSED | Instantly | Process / port | Is the app running and listening? |
| ETIMEDOUT | After your timeout | Network, firewall, or overloaded app | Does it connect at all? |
| ECONNRESET | Mid-request | App crash, proxy, keep-alive | What happened in the logs at that second? |
Why this matters for monitoring
A good monitor shouldn't just say "down". It should keep the actual error, so the person responding knows which of these three worlds they're in. When we built the checks at upordown.online, recording the error type was one of the first things we did, because "refused at 03:12 from every region" and "timeouts from one region only" call for completely different responses.
If you run your own checks, log the error code, not just a boolean.
Related reading
- No deploys on Friday: wise rule or a sign your monitoring is broken?
- How to read curl -v output to find where a request fails
- Ping is not an uptime check: 5 ways ICMP lies about whether your site is up
- Your HTTP client's default timeout is probably infinite: fix it in Python, Node, Go and curl
- Is it down, or is it me? Debugging a site that won't load, one layer at a time
Which of these three has caused you the most pain? My bet is intermittent ECONNRESET. Prove me wrong in the comments.
Top comments (0)