Home JavaScript Node ETIMEDOUT? Diagnose Network Timeouts Fast
Intermediate 7 min · September 23, 2026

Node ETIMEDOUT? Diagnose Network Timeouts Fast

Fix Node ETIMEDOUT by separating timeouts from refused and DNS faults, then adding AbortSignal timeouts and backoff retries..

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 23, 2026
last updated
1,905
articles · all by Naren
Before you start⏱ 16 min
  • Basic Node fetch/http client knowledge
  • Comfort with curl and DNS basics
  • Access to logs from a deployed service
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • ETIMEDOUT means packets went unanswered, while ECONNREFUSED means nothing listens and ENOTFOUND means DNS failed — check err.code first
  • Confirm with curl -m and DNS lookups before blaming code, since firewalls and missing egress rules cause most cases
  • Reuse connections with keep-alive agents so handshakes don't pile up under load
  • Retry transient timeouts with backoff plus AbortSignal.timeout, and fail fast on terminal errors
✦ Definition~90s read
What is Node ETIMEDOUT Network Fix?

ETIMEDOUT is a POSIX network error meaning a connection attempt or an established-socket operation received no response within the system's patience. In Node it arrives as an Error with code 'ETIMEDOUT' on net sockets, or as fetch and http failures wrapping the same cause.

Think of calling a friend who never picks up until voicemail times out — that's ETIMEDOUT.

The critical skill is distinguishing it from its siblings: ECONNREFUSED means the host answered but nothing listens on that port (fast failure, usually wrong port or stopped service); ENOTFOUND/EAI_AGAIN means DNS couldn't resolve the hostname (no packets ever left); ECONNRESET means an established connection was killed mid-stream (server crash, load-balancer cull, or idle timeout). Each code points at a different owner.

The usual production causes cluster into four buckets. Egress controls — security groups, NetworkPolicies, corporate proxies — silently drop packets to unlisted hosts, which surfaces as timeouts rather than clean rejections. Connection churn without keep-alive forces a full TCP-plus-TLS handshake per request; at 500 requests per second that's 500 handshakes competing for ephemeral ports and CPU, and the tail latency explodes.

Upstream slowness means the server accepts but answers slowly, so your client must bound its wait instead of holding sockets forever. DNS stalls delay resolution before any connection starts, and without separate DNS timeouts they masquerade as connect timeouts.

The fix stack mirrors the causes: confirm the code and the layer with curl, dig, and socket checks; open the minimal egress path; reuse connections through keep-alive agents with tuned socket counts; and wrap calls with AbortSignal.timeout plus backoff retries limited to transient errors. Timeouts are a capacity and policy problem far more often than a code bug — instrument first, tune second.

Plain-English First

Think of calling a friend who never picks up until voicemail times out — that's ETIMEDOUT. A fast busy signal would be connection refused, and dialing a number that doesn't exist would be a DNS failure. Each sound means a different problem with a different fix. Most developers hear silence and immediately rewrite their code, when they should first check whether the road between the two computers is blocked. Naming the exact failure sound cuts the debugging time from hours to minutes.

Checkout calls to your payment API start failing with ETIMEDOUT. Not all of them — 8% at peak, zero at night. Retries sometimes help, sometimes stack into a bigger pile. The firewall team says nothing changed. Your code hasn't changed either, but the error tracker shows a steady climb that started Tuesday. Welcome to timeout debugging, where the bug lives in the space between two computers.

Timeout errors mislead because they describe a symptom (no answer in time) rather than a cause (firewall drop, saturated pool, DNS stall, slow upstream). Developers infinitely tune the timeout value — 5 seconds, 30 seconds, 60 — without asking why packets go unanswered. Longer timeouts just convert fast failures into slow resource leaks.

This guide gives you the split that matters: ETIMEDOUT versus ECONNREFUSED versus ENOTFOUND, each with its confirm command. You'll learn keep-alive tuning that stops handshake pileups, and retry logic with backoff plus AbortSignal that absorbs blips without amplifying outages. Numbers included throughout.

Timeout vs Refused vs DNS: Read err.code First

Three codes, three owners, three runbooks. ETIMEDOUT means packets went out and nothing came back — suspect firewalls, egress rules, or an overloaded upstream. ECONNREFUSED means the host answered and rejected the port — suspect wrong port, stopped service, or blocked ingress. ENOTFOUND and EAI_AGAIN mean the hostname never resolved — suspect DNS config, VPC resolver, or a typo'd host. Branching on err.code routes each failure to the team that can fix it instead of dumping every network error on the app developers.

Make the split mechanical in code and in triage. Log code, syscall, address, and port on every network failure — address and port distinguish a bad config from a bad network. In triage, reproduce each layer independently: dig for DNS, nc or curl for TCP, then your Node client last. When the first two pass and Node fails, the bug is in your agent or timeout config. When dig fails, no timeout value on earth matters.

Log shape determines triage speed more than any dashboard. Emit one structured line per network failure with code, syscall, host, port, resolved IP, elapsed milliseconds, attempt number, and which deadline fired (connect, response, or total). With those fields, a single log query separates firewall drops (connect deadline, full duration, one IP) from upstream slowness (response deadline, variable durations) from DNS stalls (failure before connect, no IP). Alert on rates per code rather than totals: a spike in ETIMEDOUT pages networking, ECONNREFUSED pages the service owner, EAI_AGAIN pages platform DNS — no more all-hands for every blip. Retain the raw fields for 30 days; post-mortems routinely need the resolved IP from three weeks ago to prove a vendor migrated ranges. Good logs make the err.code split automatic instead of tribal knowledge.

src/http-client.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
async function classifyFetch(url) {
  try {
    const res = await fetch(url, { signal: AbortSignal.timeout(8000) });
    return { ok: res.ok, status: res.status };
  } catch (err) {
    const code = err.cause?.code ?? err.code ?? err.name;
    if (code === 'ETIMEDOUT' || code === 'TimeoutError') return { layer: 'network-drop' };
    if (code === 'ECONNREFUSED') return { layer: 'nothing-listening' };
    if (code === 'ENOTFOUND' || code === 'EAI_AGAIN') return { layer: 'dns' };
    return { layer: 'unknown', code };
  }
}
Try it live
📊 Production Insight
Adding code-aware logging cut network triage from 50 minutes to 8: ETIMEDOUT pages went to networking, ECONNREFUSED to the service owner, ENOTFOUND to platform DNS — no more all-hands for every blip.
🎯 Key Takeaway
Branch on err.code before anything else. Timeout, refused, and DNS failures have different owners and different fixes.

Firewalls and Egress: The Silent Packet Droppers

Firewalls that drop packets (rather than rejecting them) are invisible by design — your client waits the full timeout with zero feedback. Cloud security groups, Kubernetes NetworkPolicies, and corporate proxies all default to drop. The signature is unmistakable once you know it: instant success from an unconstrained network, full-timeout failure from production, and a vendor dashboard insisting everything is green.

Confirm from the production host itself, never from your laptop. curl with a short timeout plus dig separates DNS from transit. Compare security-group egress rules against the vendor's published IP ranges, and check whether the vendor migrated ranges recently — allowlisted IPs are a standing invitation for this outage. Log the resolved remote IP on timeouts so the next migration announces itself in your dashboards before revenue notices.

Vendor IP migrations are the recurring villain, so build defenses assuming the next one is unannounced. Subscribe to vendor status and network-change feeds, and maintain allowlists from their published ranges via infrastructure-as-code so diffs review like code. Better yet, prefer hostname-based egress (NAT gateway domain allowlists or service-mesh EgressPolicies) where your platform supports it — IPs churn, hostnames persist. Keep a runbook entry per critical vendor with its current ranges, the last-change date, and the synthetic probe URL, so the 2 AM responder isn't discovering the dependency map mid-incident. Test egress changes in staging with identical rules before promoting; security-group edits deserve the same pipeline as application code. When the next silent migration lands, your probe pages in two minutes and the fix is a reviewed range update, not a four-hour revenue investigation.

BASH
1
2
3
curl -m 10 -o /dev/null -w '%{http_code} in %{time_total}s from %{remote_ip}\n' https://api.vendor.com/health
time dig +short api.vendor.com
aws ec2 describe-security-groups --group-ids sg-0abc123 --query 'SecurityGroups[0].IpPermissionsEgress'
📊 Production Insight
A vendor migrated IP ranges overnight and 8% of checkouts timed out for 4 hours ($180K). The fix was one security-group line; the prevention was a 60-second synthetic probe that now pages in 2 minutes.
🎯 Key Takeaway
Timeout from prod plus success from laptop equals egress drop. Verify from the production host and diff vendor IP ranges.

Keep-Alive Agents: Stop Paying Handshake Tax

Without keep-alive, every HTTPS request pays a full TCP handshake plus TLS negotiation — 3-4 round trips before the first byte. At 500 requests per second that's 500 concurrent handshakes competing for ephemeral ports and CPU, and tail latency climbs exactly when traffic peaks. The symptom pattern is diagnostic: clean at night, timeouts at noon, with server CPU healthy on both sides.

A shared http.Agent with keepAlive:true reuses established connections, collapsing per-request cost to one round trip. Size maxSockets to peak concurrency plus headroom (100-200 for mid-size APIs), set keepAliveMsecs around 30 seconds to survive brief lulls, and give slow dependencies their own small agent so one sluggish host can't starve the pool. Verify with socket counters: TIME_WAIT should fall and p99 latency should drop visibly on the next peak.

Pool sizing deserves real math, not defaults. Count peak concurrent outbound requests per host (concurrency = throughput × latency: 500 rps × 0.2s = 100 sockets), add 50% headroom for bursts, and set maxSockets per host accordingly — then verify with socket counters during load tests, not production peaks. FreeSockets versus active sockets in agent.getName stats reveal whether the pool is reuse-healthy (mostly active, low churn) or thrashing (constant create-destroy). Tune keepAliveMsecs to outlast typical request gaps (30s covers most API rhythms) but stay under upstream idle timeouts — a load balancer culling at 60s turns longer keep-alives into ECONNRESET surprises. Disable Nagle (noDelay) for latency-sensitive RPC, and set scheduling priorities so bulk exports can't starve interactive checkouts sharing the pool. Measure p50, p99, and TIME_WAIT before and after; keep-alive done right shows up in all three.

src/agent.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const http = require('http');
const https = require('https');

const fastAgent = new https.Agent({
  keepAlive: true,
  keepAliveMsecs: 30000,
  maxSockets: 100,
  timeout: 8000,
});

const slowVendorAgent = new https.Agent({
  keepAlive: true,
  maxSockets: 10,
  timeout: 8000,
});

module.exports = { fastAgent, slowVendorAgent };
Try it live
💡Isolate Slow Vendors in Their Own Pool
One shared pool lets a single sluggish host consume every socket. Give slow dependencies a small dedicated agent so their timeouts can't cascade into healthy routes.
📊 Production Insight
Enabling keep-alive with maxSockets 100 cut p99 checkout latency 22% and eliminated noon timeout spikes that had paged weekly for two months.
🎯 Key Takeaway
Reuse connections with keep-alive agents sized to peak load. Handshake churn is the top cause of peak-only timeouts.

Retry With Backoff, Not With Hope

Transient blips deserve retries; persistent outages deserve fast failure. The discipline is threefold: retry only transient signals (ETIMEDOUT, ECONNRESET, 503, 429), space attempts with exponential backoff plus jitter so clients don't thundering-herd the recovering server, and cap the total budget under the caller's patience. A retry policy without a budget just moves the queue from the vendor to your event loop.

Bound every attempt with AbortSignal.timeout so a hung socket can't consume the whole budget. Add jitter (randomize each delay by 20-50%) to desynchronize fleet-wide retries. Count attempts in logs and export retry-rate metrics — a rising retry rate is the earliest warning of upstream trouble, visible minutes before timeouts breach SLO. When retries exhaust, return 502 with Retry-After rather than hanging: a fast honest failure beats a slow ambiguous one.

Idempotency keys make retries safe, so design them per operation. Derive keys deterministically from business identity — order ID plus attempt scope for payments, event ID for webhooks, request hash for reads — so replays collapse onto the original instead of duplicating side effects. Send keys on every mutating call, not just payments: inventory decrements, email sends, and provisioning calls all double-fire under retry without them. Store key-to-result mappings server-side with TTLs covering your maximum retry window, and return the stored result on key replay with a replayed: true flag for observability. Test the path explicitly: simulate a timeout-after-commit in staging (proxy that drops the response but forwards the request) and confirm the retry returns the original without duplication. Without this test, idempotency is a header you hope works rather than a guarantee you've proven.

src/retry.jsJAVASCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
const TRANSIENT = new Set(['ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN']);

async function fetchWithRetry(url, { attempts = 3, baseMs = 400 } = {}) {
  for (let i = 1; i <= attempts; i++) {
    try {
      return await fetch(url, { signal: AbortSignal.timeout(8000) });
    } catch (err) {
      const code = err.cause?.code ?? err.code;
      if (!TRANSIENT.has(code) || i === attempts) throw err;
      const wait = baseMs * 2 ** (i - 1) * (0.8 + Math.random() * 0.4);
      await new Promise((r) => setTimeout(r, wait));
    }
  }
}
Try it live
📊 Production Insight
Three-attempt backoff with jitter absorbed a 12-minute vendor brownout: 1,900 retries, zero user-facing failures, and the retry-rate metric paged 9 minutes before timeouts would have.
🎯 Key Takeaway
Retry transient errors only, with backoff, jitter, per-attempt timeouts, and a hard budget. Export retry rates as an early warning.

AbortSignal.timeout: Bound Every Wait

A request without a timeout is a resource leak with optimistic branding. Hung sockets pin pool slots, accumulate memory, and cascade one slow dependency into every route sharing the pool. Node's AbortSignal.timeout(8000) gives fetch a clean per-attempt deadline; for raw http requests, set request.setTimeout plus socket timeouts. The value should reflect user patience (5-10 seconds for interactive paths) rather than vendor promises.

Keep three timeouts distinct: connect timeout (can we reach them), response timeout (do they answer), and total budget (how long do we keep trying). Conflating them produces 30-second hangs on paths users abandon in 3. Log which deadline fired — connect timeouts implicate the network, response timeouts implicate the upstream — and size pools so a burst of timeouts degrades one route, not the server.

Layer timeouts like defense in depth. Set a connect timeout (2-3s: can we reach them), a response timeout per attempt (5-8s: do they answer), and a total budget across retries (under the caller's patience — 15s for interactive paths). Each layer implicates a different owner when it fires, and the total budget is what prevents retry logic from converting a vendor outage into your own. In Node's http module, setTimeout on the request plus destroy on timeout approximates what fetch gets for free with AbortSignal; for streams, abort the stream and release the socket or the pool leaks exactly as if no timeout existed. Propagate deadlines downstream: pass the remaining budget to chained calls via headers so depth-three fan-outs can't each spend the full allowance. Review timeout values quarterly against actual latency percentiles — budgets set during an outage tend to stay generous forever.

BASH
1
2
node -e "fetch('https://httpbin.org/delay/10', { signal: AbortSignal.timeout(3000) }).then(r => console.log(r.status)).catch(e => console.error('deadline fired:', e.name))"
node -e "console.log(typeof AbortSignal.timeout === 'function' ? 'abort-timeout ok' : 'upgrade node')"
📊 Production Insight
Cutting the vendor timeout from 30 back to 8 seconds with per-attempt aborts freed 70% of pool sockets during the next brownout. Unrelated routes stayed green throughout.
🎯 Key Takeaway
Every outbound call gets a deadline. Timeouts bound blast radius; unbounded waits spread it.

DNS: The Timeout Impostor

Slow or broken DNS looks exactly like a connection timeout when you only watch total duration. Resolution happens before any packet leaves, so a 5-second resolver stall plus a fast connection still reports as a timeout against your 8-second budget. In containers the usual culprits are missing VPC DNS, ndots misconfiguration causing search-domain storms, and resolver IPs that answer intermittently.

Separate DNS from TCP explicitly: time dig independently, log resolution duration in your client, and cache successful lookups with a TTL-aware cache. Prefer all:false plus family hints to avoid Happy Eyeballs delays on misconfigured IPv6. When EAI_AGAIN clusters across services simultaneously, page platform DNS — no application retry policy fixes a resolver outage, and fast failure preserves your pools for recovery.

Resolver hygiene prevents the impostor class entirely. Run a local caching resolver (Node's built-in cache with TTL, or a sidecar like dnsmasq) so transient upstream blips don't stall every new connection. Audit ndots and search domains in container resolv.conf — ndots:5 with three search domains turns one external lookup into a storm of doomed queries, each adding latency before the real resolution even starts. Pin critical dependencies to dual-stack-aware lookup (verbatim ordering, explicit family) to avoid Happy Eyeballs delays on half-broken IPv6. Monitor DNS resolution time as its own metric with alerts at p99 thresholds; when it degrades fleet-wide, the dashboard should say DNS before anyone blames application timeouts. And keep a static-host fallback for the two dependencies that would halt revenue — written by hand, reviewed quarterly, and worth every line during a resolver outage.

BASH
1
2
3
time dig +short api.vendor.com
cat /etc/resolv.conf
node -e "const t=Date.now(); require('dns').lookup('api.vendor.com', e => console.log(e && e.code, Date.now() - t + 'ms'))"
📊 Production Insight
A ndots:5 misconfig turned every external call into 6 DNS queries; p99 latency carried a hidden 400ms tax for months until independent dig timing exposed it.
🎯 Key Takeaway
Time DNS separately from TCP. Resolver stalls masquerade as timeouts and need platform fixes, not longer deadlines.
● Production incidentPOST-MORTEMseverity: high

Checkout Lost $180K in 4 Hours: A Missing Egress Rule

Symptom
On a Thursday at 11 AM, checkout success dropped from 99.2% to 91.4% during peak traffic. The error tracker showed 2,300 ETIMEDOUT errors per hour against fraud-check.vendor.com, all with 10-second durations matching the client timeout. The vendor status page stayed green because their servers were healthy — packets from our VPC never reached them. Worst of all, the failures correlated with order value: high-value orders triggered the fraud check while low-value ones skipped it, so revenue impact ran 3x the error rate. Four hours cost roughly $180,000 in abandoned carts.
Assumption
The team blamed the vendor's latency and raised the timeout from 10 to 30 seconds. Timeout errors dropped briefly — then connection-pool exhaustion set in as hung sockets piled up, and the API started returning 503s for unrelated routes. A second engineer blamed DNS and switched resolvers, which changed nothing because resolution succeeded in 12ms. The firewall rule was last on the suspect list because no firewall change had been announced — but the vendor had migrated IP ranges the previous night without telling customers behind allowlisted ranges.
Root cause
The vendor migrated fraud-check endpoints from 203.0.113.0/24 to 198.51.100.0/24 overnight. Our egress security group allowlisted only the old range, so SYN packets to new IPs were silently dropped — the textbook signature of ETIMEDOUT rather than ECONNREFUSED. Only high-value checkouts called the vendor, explaining the 8% rate and the revenue skew. Raising the timeout worsened the blast radius by pinning pool sockets 3x longer per doomed request.
Fix
The network team added the new /24 to the egress group within 20 minutes of diagnosis, restoring success to 99.1%. The app team then reverted the timeout to 8 seconds with AbortSignal, added a keep-alive agent (maxSockets 100) to survive traffic spikes without handshake storms, and built a synthetic probe hitting the vendor every 60 seconds with an alert on two consecutive timeouts. A vendor-change calendar subscription now flags IP migrations a week ahead. Follow-up load tests showed p99 checkout latency down 22% from the keep-alive change alone.
Key lesson
  • Silent packet drops always surface as timeouts. When curl connects fast from your laptop but times out from production, compare egress rules before touching timeout values.
  • Raising timeouts without diagnosis amplifies the outage. Longer waits pin sockets and pools, converting a partial failure into a full one — bound waits and retry with backoff instead.
  • Probe third-party dependencies synthetically every minute. A 60-second canary would have paged in 2 minutes instead of letting revenue bleed for 4 hours.
Production debug guideSix checks that isolate the failing layer — run from outside in before changing any timeout value.6 entries
Symptom · 01
Node throws ETIMEDOUT calling a third-party host
Fix
Split network from code: run curl -m 10 -o /dev/null -w '%{http_code} %{time_total}s\n' https://vendor-host/health and time dig +short vendor-host from the production host. If curl times out but dig resolves in milliseconds, packets are dropped in transit — check egress rules. If curl works, the fault is in your agent or pool config.
Symptom · 02
Error code is ECONNREFUSED instead of ETIMEDOUT
Fix
That's a different runbook: run nc -vz target-host 443 or node -e "require('net').connect(443,'host').on('connect',()=>console.log('open')).on('error',e=>console.error(e.code))" to confirm nothing listens. Fix the port, security-group ingress, or start the upstream service — no timeout tuning will help a refused port.
Symptom · 03
Error code is ENOTFOUND or EAI_AGAIN
Fix
Debug DNS, not TCP: run dig +trace api-host and cat /etc/resolv.conf on the production host, plus node -e "require('dns').lookup('api-host',console.log)" to compare resolver behavior. Fix broken resolver IPs, missing VPC DNS, or unqualified names needing a search domain — then add DNS caching to survive resolver blips.
Symptom · 04
Timeouts spike only at peak traffic, clean at night
Fix
Suspect handshake churn and pool saturation: run ss -s and node -e "console.log(process.memoryUsage())" during peak, and check for missing keep-alive with grep -rn 'keepAlive\|Agent' src/. Add an http.Agent with keepAlive:true and maxSockets sized to peak concurrency, then verify TIME_WAIT drops with ss -ant | grep -c TIME-WAIT.
Symptom · 05
Requests hang exactly to your timeout value, then fail in waves
Fix
Bound and back off: wrap calls with AbortSignal.timeout(8000) and retry transient codes only, verifying with node -e "fetch(url,{signal:AbortSignal.timeout(3000)}).catch(e=>console.error(e.name))". Keep the timeout tight, cap retries at 3 with exponential backoff, and alert on retry-rate spikes that signal upstream trouble.
Symptom · 06
One dependency's slowness cascades into unrelated routes failing
Fix
Isolate with bulkheads: run curl -m 5 on the slow endpoint to confirm, then give that client its own agent with a small maxSockets plus a circuit breaker that fails fast with 502 after consecutive timeouts. Verify isolation by loading the slow path and watching other routes stay green.
ETIMEDOUT and Friends — Causes Compared
Root CauseHow to ConfirmFixPrevention
Egress drop / firewallcurl times out from prod, works elsewhereAllowlist vendor rangeSynthetic probe every 60s
Nothing listening (refused)nc fails fast with refusedFix port or start serviceDeploy-time port checks
DNS failure (ENOTFOUND)dig fails; lookup errors fastFix resolver or hostnameDNS caching plus monitoring
Handshake churn, no keep-alivePeak-only timeouts, high TIME_WAITKeep-alive agent, sized socketsLoad-test peaks with pools
Slow upstream, no deadlineHangs exactly to timeout valueAbortSignal.timeout plus backoffSLO budgets per dependency
⚙ Quick Reference
6 commands from this guide
FileCommand / CodePurpose
srchttp-client.jsasync function classifyFetch(url) {Timeout vs Refused vs DNS
curl -m 10 -o /dev/null -w '%{http_code} in %{time_total}s from %{remote_ip}\n' ...Firewalls and Egress
srcagent.jsconst http = require('http');Keep-Alive Agents
srcretry.jsconst TRANSIENT = new Set(['ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN']);Retry With Backoff, Not With Hope
node -e "fetch('https://httpbin.org/delay/10', { signal: AbortSignal.timeout(300...AbortSignal.timeout
time dig +short api.vendor.comDNS

Key takeaways

1
Read err.code first
timeout, refused, and DNS are different runbooks.
2
Verify from the production host
laptop success proves nothing.
3
Reuse connections with keep-alive agents sized to peak load.
4
Bound every call with AbortSignal.timeout and a retry budget.
5
Retry transient errors with backoff and jitter; fail fast otherwise.
6
Probe vendors synthetically so their changes page you in minutes.

Common mistakes to avoid

6 patterns
×

Raising the timeout instead of diagnosing the layer

Symptom
Failures slow down but spread: pools exhaust and healthy routes start 503ing.
Fix
Confirm with curl and dig from prod first; bound waits and add backoff instead of inflating them.
×

Treating ECONNREFUSED like a timeout

Symptom
Retries hammer a port nothing listens on while the stopped service stays down.
Fix
Branch on err.code: refused means fix the port or service, not the patience.
×

Sharing one pool across fast and slow hosts

Symptom
One sluggish vendor consumes every socket and cascades into unrelated routes.
Fix
Give slow dependencies small dedicated agents with their own timeouts.
×

Retrying terminal errors

Symptom
400s and 401s multiply across retries, tripping rate limits and locking accounts.
Fix
Retry transient codes only; fail fast on client errors with clear status codes.
×

Skipping synthetic probes for vendors

Symptom
Vendor-side changes bleed revenue for hours before anyone notices.
Fix
Probe each dependency every 60 seconds and alert on two consecutive failures.
×

Lumping DNS stalls into connect timeouts

Symptom
Resolver outages trigger app-level retry storms that worsen recovery.
Fix
Time DNS separately, cache resolutions, and fail fast on EAI_AGAIN clusters.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
How do you separate ETIMEDOUT from ECONNREFUSED and ENOTFOUND?
Q02SENIOR
Timeouts spike only at peak traffic. What's your prime suspect and fix?
Q03SENIOR
Why did raising the timeout from 10 to 30 seconds make the outage worse?
Q04SENIOR
Design retries for a flaky vendor without amplifying their outage.
Q05SENIOR
Every service times out simultaneously with EAI_AGAIN. What do you do?
Q01 of 05JUNIOR

How do you separate ETIMEDOUT from ECONNREFUSED and ENOTFOUND?

ANSWER
Timeout means packets unanswered — suspect firewall or overload. Refused means host answered, port closed — suspect wrong port or stopped service. ENOTFOUND means DNS failed — suspect resolver. I'd confirm with dig, nc, and curl in that order.
FAQ · 6 QUESTIONS

Frequently Asked Questions

01
What timeout value should I use?
02
Should GET and POST retry the same way?
03
How do I size maxSockets?
04
Why does curl work but Node time out?
05
Do proxies change timeout debugging?
06
When should I page versus retry quietly?
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 23, 2026
last updated
1,905
articles · all by Naren
🔥

That's Node.js. Mark it forged?

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

Previous
Unhandled Promise Rejection Fix
26 / 30 · Node.js
Next
ERR OSSL EVP Unsupported Fix