Fix 'Error: connect ETIMEDOUT' When Connecting to Database or External Service in Node.js

intermediate๐Ÿ’š Node.js2026-04-02| Node.js (v14+), Linux/macOS/Windows, works with MySQL, PostgreSQL, MongoDB, Redis, HTTP clients (axios, node-fetch)

Error Message

Error: connect ETIMEDOUT <IP>:<PORT>
#nodejs#network#timeout#database#http

TL;DR

Your Node.js process sent a TCP connection request and got... nothing back. No response, just silence until the timeout kicked in. That's ETIMEDOUT. The usual suspects: a firewall swallowing your packets, a wrong IP, the database isn't actually running, or the server is too slammed to respond. Run telnet or nc first to confirm it's a network issue, then hunt down the specific blocker.

What This Error Means

The full stack trace looks like this:

Error: connect ETIMEDOUT 203.0.113.42:5432
    at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1144:16) {
  errno: -110,
  code: 'ETIMEDOUT',
  syscall: 'connect',
  address: '203.0.113.42',
  port: 5432
}

Node.js sent a TCP SYN packet to port 5432. No SYN-ACK came back. After the OS timeout window (typically 20โ€“75 seconds), it gave up.

This is different from ECONNREFUSED, where the remote host actively rejects your connection. With ETIMEDOUT, packets just vanish โ€” into a firewall, a dead route, or a service too overwhelmed to respond. That distinction matters when debugging: ECONNREFUSED means the host is reachable, ETIMEDOUT means something in between is blocking you.

Root Causes

  • Firewall blocking the port โ€” the #1 cause on AWS, GCP, and any hardened Linux server. Security Groups, UFW, and iptables all silently drop packets by default.
  • Wrong IP address or hostname โ€” the service is alive, you're just pointing at the wrong address. A typo in .env can waste an hour.
  • Service not listening on that port โ€” database crashed, or it bound to 127.0.0.1 instead of 0.0.0.0.
  • Network routing issue โ€” VPN disconnected, VPC peering not configured, or simply no route to the host.
  • Connection pool exhausted โ€” all 10 connections are busy, new requests queue up, then die waiting for a slot.

Step 1 โ€” Test Connectivity Before Touching Code

Don't start tweaking timeouts or rewriting connection logic yet. Confirm first whether this is a network problem or an application problem:

# Test TCP connectivity directly
telnet 203.0.113.42 5432

# No telnet? Use nc instead
nc -zv 203.0.113.42 5432 -w 5

# Verify DNS resolves to the right IP
nslookup your-db-host.example.com
dig your-db-host.example.com

Three outcomes to watch for:

  • Hangs indefinitely โ†’ firewall is dropping packets. Go to Step 2.
  • Connection refused โ†’ host is reachable but nothing is listening. Go to Step 3.
  • Connects immediately โ†’ network is fine. The bug is in your Node.js config. Go to Step 4.

Step 2 โ€” Unblock the Firewall

Cloud firewalls silently drop packets โ€” that's exactly what ETIMEDOUT looks like from the outside.

AWS EC2 / RDS

# Inspect security group inbound rules
aws ec2 describe-security-groups --group-ids sg-xxxxxxxx

# For RDS: Console โ†’ RDS โ†’ Your DB โ†’ Connectivity & security โ†’ VPC security groups

The inbound rule must allow TCP on your target port from your app server's IP or security group. A common mistake: allowing 0.0.0.0/0 in dev but forgetting to add the app server's security group ID in production.

Linux server (UFW)

# See current rules
sudo ufw status verbose

# Open a port globally
sudo ufw allow 5432/tcp

# Or restrict to a specific IP (safer in production)
sudo ufw allow from 10.0.1.50 to any port 5432

iptables

sudo iptables -L INPUT -n -v | grep 5432

Step 3 โ€” Verify the Service Is Actually Listening

SSH into the database server and check what's bound to the port:

# See which process is listening, and on which interface
ss -tlnp | grep 5432
# or
netstat -tlnp | grep 5432

# Check service health
systemctl status postgresql
systemctl status mysql
systemctl status mongod

The bound address tells the whole story. 127.0.0.1:5432 means the service only accepts local connections. 0.0.0.0:5432 means it's open to all interfaces (firewall rules still apply).

Seeing 127.0.0.1? Fix it in the service config. For PostgreSQL, open /etc/postgresql/*/main/postgresql.conf and update this line:

listen_addresses = 'localhost'   # change this
listen_addresses = '*'           # to this, then restart postgresql

Step 4 โ€” Tune Timeout Settings in Node.js

Network checks out fine but timeouts still happen under load? Connection pool exhaustion is the likely culprit. New requests queue up waiting for a free slot, wait too long, and die with ETIMEDOUT.

With pg (PostgreSQL)

const { Pool } = require('pg');

const pool = new Pool({
  host: 'your-db-host',
  port: 5432,
  database: 'mydb',
  user: 'user',
  password: 'password',
  connectionTimeoutMillis: 10000,  // wait up to 10s for a free connection
  max: 20,                          // up from default 10 if your DB can handle it
  idleTimeoutMillis: 30000,
});

With mysql2

const mysql = require('mysql2/promise');

const pool = mysql.createPool({
  host: 'your-db-host',
  port: 3306,
  user: 'user',
  password: 'password',
  database: 'mydb',
  connectTimeout: 10000,
  waitForConnections: true,
  connectionLimit: 10,
  queueLimit: 0
});

With axios (HTTP requests)

const axios = require('axios');

const client = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,  // 10 seconds
});

try {
  const response = await client.get('/endpoint');
} catch (err) {
  if (err.code === 'ETIMEDOUT' || err.code === 'ECONNABORTED') {
    console.error('Request timed out โ€” check network or bump the timeout value');
  }
  throw err;
}

Step 5 โ€” Add Retry Logic for Transient Failures

Not every ETIMEDOUT means something is broken. Brief network hiccups, rolling restarts, and cold-start delays can all trigger a one-off timeout that resolves on its own. A simple retry with exponential backoff handles these without alerting anyone:

async function connectWithRetry(connectFn, retries = 3, delay = 2000) {
  for (let i = 0; i  setTimeout(res, delay * (i + 1)));
      } else {
        throw err;
      }
    }
  }
}

// Usage
await connectWithRetry(() => pool.query('SELECT 1'));

This retries up to 3 times with delays of 2s, 4s, and 6s. Adjust based on how quickly your service typically recovers.

Verify the Fix

Once you've made a change, confirm it actually worked before redeploying:

# Quick TCP test from the app server
nc -zv your-db-host 5432 -w 5
# Expected: Connection to your-db-host 5432 port [tcp/postgresql] succeeded!

# Or test directly with Node.js
node -e "
const net = require('net');
const socket = net.createConnection({ host: 'your-db-host', port: 5432, timeout: 5000 });
socket.on('connect', () => { console.log('Connected!'); socket.destroy(); });
socket.on('timeout', () => { console.log('Timed out'); socket.destroy(); });
socket.on('error', (err) => console.log('Error:', err.message));
"

Tips

When debugging firewall rules, you often need to check whether your app server's IP falls within an allowed CIDR block. The Subnet Calculator on ToolCraft handles this quickly โ€” paste in your CIDR and IP, and it tells you immediately if there's a match. Runs entirely in the browser, no data sent anywhere. Handy when you're staring at an AWS Security Group rule like 10.0.0.0/16 and wondering if 10.0.1.50 is actually in range.

Quick Reference

  • ETIMEDOUT โ€” packets sent, no response (firewall or dead route)
  • ECONNREFUSED โ€” host reached, port rejected the connection (service down)
  • ENOTFOUND โ€” DNS resolution failed (wrong hostname)
  • ECONNRESET โ€” connection established, then abruptly closed by remote

Related Error Notes