Sửa lỗi 'Error: connect ETIMEDOUT' khi kết nối Database hoặc Dịch vụ Ngoài trong Node.js

intermediate💚 Node.js2026-04-02| Node.js (v14+), Linux/macOS/Windows, hỗ trợ MySQL, PostgreSQL, MongoDB, Redis, HTTP client (axios, node-fetch)

Error Message

Error: connect ETIMEDOUT <IP>:<PORT>
#nodejs#mạng#timeout#database#http

TL;DR

Tiến trình Node.js của bạn đã gửi yêu cầu kết nối TCP nhưng không nhận được phản hồi. Hoàn toàn im lặng cho đến khi timeout xảy ra. Đó chính là ETIMEDOUT. Các nguyên nhân phổ biến nhất: firewall nuốt mất gói tin, sai địa chỉ IP, database không thực sự đang chạy, hoặc server quá tải đến mức không thể phản hồi. Hãy chạy telnet hoặc nc trước để xác nhận đây là vấn đề mạng, rồi mới truy tìm nguyên nhân cụ thể.

Ý Nghĩa Của Lỗi Này

Stack trace đầy đủ trông như sau:

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 đã gửi gói TCP SYN đến cổng 5432. Không có SYN-ACK nào trả về. Sau khoảng thời gian timeout của hệ điều hành (thường từ 20–75 giây), kết nối bị hủy.

Lỗi này khác với ECONNREFUSED, khi máy chủ từ xa chủ động từ chối kết nối của bạn. Với ETIMEDOUT, các gói tin đơn giản là biến mất — vào firewall, một tuyến đường đã chết, hoặc một dịch vụ quá tải. Sự khác biệt này rất quan trọng khi debug: ECONNREFUSED nghĩa là host có thể truy cập được, còn ETIMEDOUT nghĩa là có thứ gì đó ở giữa đang chặn bạn.

Nguyên Nhân Gốc Rễ

  • Firewall chặn cổng — nguyên nhân số 1 trên AWS, GCP và mọi Linux server được cấu hình bảo mật. Security Groups, UFW và iptables đều âm thầm bỏ gói tin theo mặc định.
  • Sai địa chỉ IP hoặc hostname — dịch vụ đang hoạt động bình thường, nhưng bạn đang trỏ vào địa chỉ sai. Một lỗi đánh máy trong .env có thể làm mất cả tiếng đồng hồ.
  • Dịch vụ không lắng nghe trên cổng đó — database bị crash, hoặc nó bind vào 127.0.0.1 thay vì 0.0.0.0.
  • Sự cố định tuyến mạng — VPN bị ngắt kết nối, VPC peering chưa được cấu hình, hoặc đơn giản là không có tuyến đường đến host.
  • Connection pool cạn kiệt — cả 10 kết nối đều đang bận, các yêu cầu mới phải xếp hàng chờ, rồi chết vì chờ quá lâu.

Bước 1 — Kiểm Tra Kết Nối Trước Khi Đụng Vào Code

Đừng vội chỉnh sửa timeout hay viết lại logic kết nối. Hãy xác nhận trước xem đây là vấn đề mạng hay vấn đề ứng dụng:

# Kiểm tra kết nối TCP trực tiếp
telnet 203.0.113.42 5432

# Không có telnet? Dùng nc thay thế
nc -zv 203.0.113.42 5432 -w 5

# Xác minh DNS phân giải đúng IP
nslookup your-db-host.example.com
dig your-db-host.example.com

Có ba kết quả cần chú ý:

  • Treo vô thời hạn → firewall đang bỏ gói tin. Chuyển sang Bước 2.
  • Connection refused → host có thể truy cập nhưng không có gì đang lắng nghe. Chuyển sang Bước 3.
  • Kết nối ngay lập tức → mạng ổn. Lỗi nằm trong cấu hình Node.js của bạn. Chuyển sang Bước 4.

Bước 2 — Mở Chặn Firewall

Firewall trên cloud âm thầm bỏ gói tin — đó chính xác là những gì ETIMEDOUT trông như thế nào từ bên ngoài.

AWS EC2 / RDS

# Kiểm tra các quy tắc inbound của security group
aws ec2 describe-security-groups --group-ids sg-xxxxxxxx

# Với RDS: Console → RDS → DB của bạn → Connectivity & security → VPC security groups

Quy tắc inbound phải cho phép TCP trên cổng đích từ IP của app server hoặc security group. Lỗi thường gặp: cho phép 0.0.0.0/0 trong môi trường dev nhưng quên thêm security group ID của app server trong production.

Linux server (UFW)

# Xem các quy tắc hiện tại
sudo ufw status verbose

# Mở cổng cho tất cả
sudo ufw allow 5432/tcp

# Hoặc giới hạn theo IP cụ thể (an toàn hơn trong production)
sudo ufw allow from 10.0.1.50 to any port 5432

iptables

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

Bước 3 — Xác Minh Dịch Vụ Đang Thực Sự Lắng Nghe

SSH vào database server và kiểm tra xem cái gì đang bind vào cổng:

# Xem tiến trình nào đang lắng nghe và trên interface nào
ss -tlnp | grep 5432
# hoặc
netstat -tlnp | grep 5432

# Kiểm tra trạng thái dịch vụ
systemctl status postgresql
systemctl status mysql
systemctl status mongod

Địa chỉ bind nói lên tất cả. 127.0.0.1:5432 nghĩa là dịch vụ chỉ chấp nhận kết nối cục bộ. 0.0.0.0:5432 nghĩa là nó mở cho tất cả các interface (các quy tắc firewall vẫn áp dụng).

Thấy 127.0.0.1? Hãy sửa trong file cấu hình dịch vụ. Với PostgreSQL, mở /etc/postgresql/*/main/postgresql.conf và cập nhật dòng này:

listen_addresses = 'localhost'   # đổi dòng này
listen_addresses = '*'           # thành dòng này, rồi restart postgresql

Bước 4 — Điều Chỉnh Cài Đặt Timeout Trong Node.js

Mạng hoạt động bình thường nhưng timeout vẫn xảy ra khi tải cao? Connection pool cạn kiệt là thủ phạm có khả năng nhất. Các yêu cầu mới xếp hàng chờ kết nối rảnh, chờ quá lâu rồi chết với lỗi ETIMEDOUT.

Với pg (PostgreSQL)

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

const pool = new Pool({
  host: 'your-db-host',
  port: 5432,
  database: 'mydb',
  user: 'user',
  password: 'password',
  connectionTimeoutMillis: 10000,  // chờ tối đa 10s để có kết nối rảnh
  max: 20,                          // tăng từ mặc định 10 nếu DB của bạn xử lý được
  idleTimeoutMillis: 30000,
});

Với 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
});

Với axios (HTTP requests)

const axios = require('axios');

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

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;
}

Bước 5 — Thêm Logic Retry Cho Lỗi Tạm Thời

Không phải mọi ETIMEDOUT đều có nghĩa là có sự cố. Các trục trặc mạng ngắn, rolling restart và độ trễ cold-start đều có thể gây ra timeout một lần rồi tự giải quyết. Một cơ chế retry đơn giản với exponential backoff xử lý các trường hợp này mà không cần cảnh báo ai:

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

// Sử dụng
await connectWithRetry(() => pool.query('SELECT 1'));

Hàm này retry tối đa 3 lần với độ trễ lần lượt là 2s, 4s và 6s. Điều chỉnh tùy theo tốc độ phục hồi thông thường của dịch vụ bạn.

Xác Nhận Kết Quả Sửa Lỗi

Sau khi thực hiện thay đổi, hãy xác nhận nó thực sự hoạt động trước khi redeploy:

# Kiểm tra TCP nhanh từ app server
nc -zv your-db-host 5432 -w 5
# Kết quả mong đợi: Connection to your-db-host 5432 port [tcp/postgresql] succeeded!

# Hoặc kiểm tra trực tiếp bằng 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));
"

Mẹo Hữu Ích

Khi debug các quy tắc firewall, bạn thường cần kiểm tra xem IP của app server có nằm trong dải CIDR được phép hay không. Subnet Calculator trên ToolCraft xử lý điều này nhanh chóng — chỉ cần dán CIDR và IP vào, nó sẽ cho bạn biết ngay có khớp hay không. Chạy hoàn toàn trên trình duyệt, không gửi dữ liệu đi đâu cả. Rất tiện khi bạn đang nhìn chằm chằm vào quy tắc AWS Security Group như 10.0.0.0/16 và tự hỏi liệu 10.0.1.50 có thực sự nằm trong dải đó không.

Tham Khảo Nhanh

  • ETIMEDOUT — đã gửi gói tin nhưng không có phản hồi (firewall hoặc tuyến đường chết)
  • ECONNREFUSED — đã đến host, cổng từ chối kết nối (dịch vụ bị down)
  • ENOTFOUND — phân giải DNS thất bại (sai hostname)
  • ECONNRESET — đã thiết lập kết nối, sau đó bị máy chủ từ xa đóng đột ngột

Related Error Notes