ECONNREFUSED, ECONNRESET, ETIMEDOUT — đọc đúng nguyên nhân
Bốn lỗi mạng kinh điển, mỗi lỗi ứng với chặng nào hỏng và cách xác định nhanh bằng ss, nc, tcpdump.
TL;DR: ECONNREFUSED — server trả RST ngay vì không ai listen ở port đó. ECONNRESET — kết nối đang mở bị phía kia gửi RST đột ngột: server crash, idle timeout LB, hay pool giữ connection chết. ETIMEDOUT — gói bị nuốt im lặng (firewall DROP, route hỏng, server quá tải), không có RST nào về. EADDRINUSE — port bị giữ bởi TIME_WAIT hoặc process khác. EPIPE — ghi vào socket sau khi peer đã đóng. Bảng tra cuối bài giúp khoanh vùng nguyên nhân và chọn công cụ đúng.
Bạn đang on-call lúc 2 giờ sáng, alert báo service Java của bạn bắt đầu bùng lỗi java.net.ConnectException. Log có khi ghi ECONNREFUSED, có khi ghi ETIMEDOUT, đôi lúc lại ECONNRESET. Ba cái tên trông giống nhau — nhưng mỗi cái chỉ vào một chặng hỏng hoàn toàn khác nhau. Nhầm chẩn đoán là mất 30 phút debug sai hướng.
Bài này giải phẫu từng lỗi: RST gửi từ đâu, DROP khác REJECT thế nào, connection pool gây ECONNRESET qua cơ chế gì — để bạn đọc log và khoanh vùng nguyên nhân trong vòng 2 phút.
1. Analogy — Gọi điện, gõ cửa, và thư gửi đi không có người nhận
Hình dung ba kịch bản liên lạc:
- Bạn gọi điện tới số không tồn tại → tổng đài báo ngay "số này không có thực" và tắt máy. Nhanh, dứt khoát.
- Bạn đang nói chuyện dở thì đầu kia cúp máy không báo → bạn nghe tiếng "tít tít" và biết mình bị ngắt.
- Bạn gửi thư bảo đảm tới địa chỉ đúng, bưu điện nhận thư, nhưng thư không bao giờ tới và bạn không nhận hồi âm → thư bị giữ ở đâu đó, bạn chờ mãi.
| Kịch bản đời thường | Lỗi mạng |
|---|---|
| Số không tồn tại → tổng đài báo ngay | ECONNREFUSED — server gửi RST tức thì |
| Đang nói chuyện bị cúp máy đột ngột | ECONNRESET — peer gửi RST giữa chừng |
| Thư gửi đi nhưng không có hồi âm | ETIMEDOUT — gói bị DROP im lặng |
| Số điện thoại đang bận | EADDRINUSE — port bị chiếm |
| Ghi vào máy fax đã tắt | EPIPE — ghi vào socket peer đã đóng |
Refused = ai đó chủ động từ chối (nhanh, có RST). Timeout = im lặng tuyệt đối (chậm, không có RST). Reset = đang nói chuyện bị cúp giữa chừng. Phân biệt ba cái này thu hẹp không gian nguyên nhân gần 80%.
2. ECONNREFUSED — không có process nào đang listen
ECONNREFUSED xảy ra khi client gửi gói SYN tới một cổng và hệ điều hành phía server trả về gói RST (reset) ngay lập tức. RST nghĩa là: "tôi không có kết nối nào liên quan tới tuple này."
Nguyên nhân phổ biến nhất: không có process nào đang listen() ở port đó. Ví dụ server Spring Boot chưa start xong, service restart giữa chừng, hoặc bạn deploy nhầm port.
Cơ chế bên dưới: khi OS nhận một gói SYN tới port X mà không có socket nào ở trạng thái LISTEN tại port X, kernel tự động tạo và gửi gói RST. Không cần bất kỳ process nào tham gia — kernel làm luôn. Đây là lý do lỗi đến tức thì, không phải sau vài giây chờ.
Client Server OS
|---SYN (port 8080)---->|
|<--RST-----------------| (kernel tu tra, khong can process)
ConnectException: Connection refused
Khác gì firewall DROP? Đây là pitfall quan trọng nhất của bài:
ECONNREFUSEDđến tức thì (dưới 1ms) — server OS nhận được SYN và trả RST.ETIMEDOUTđến sau vài chục giây — firewall chặn gói (DROP), server không bao giờ nhận được SYN.
Nếu lỗi đến nhanh → nghi không ai listen. Nếu lỗi đến chậm → nghi bị chặn hoặc route hỏng.
Chẩn đoán:
# Kiem tra process nao dang listen port 8080
ss -tlnp sport = :8080
# Thu ket noi toi host:port
nc -zv 10.0.0.5 8080
# Output: Connection refused (ECONNREFUSED ngay lap tuc)
# Xem goi RST trong Wireshark/tcpdump
sudo tcpdump -n 'tcp port 8080 and tcp[tcpflags] & tcp-rst != 0'
Một firewall chặn kết nối thường cấu hình DROP (im lặng), không phải REJECT. Lỗi nhận được sẽ là ETIMEDOUT sau timeout — không phải ECONNREFUSED. Nếu bạn thấy ECONNREFUSED, nghĩa là gói SYN đã đến được server OS — firewall không chặn. Vấn đề nằm ở tầng process.
3. ECONNRESET — kết nối bị phá giữa chừng
ECONNRESET xảy ra sau khi kết nối đã được thiết lập (ESTABLISHED). Peer gửi gói RST để hủy kết nối đột ngột, không qua quá trình đóng 4 bước bình thường (FIN/FIN-ACK).
Cơ chế: TCP connection được định danh bởi tuple 4 giá trị: (src IP, src port, dst IP, dst port). Khi một bên nhận gói RST khớp với connection đang mở, nó lập tức loại bỏ kết nối — mọi dữ liệu đang trong buffer bị xóa.
Nguyên nhân phổ biến:
a) Server crash hoặc restart giữa chừng. Process bị kill → OS gửi RST cho mọi socket đang mở. Client đang chờ response nhận RST và báo ECONNRESET.
b) Idle timeout từ Load Balancer. LB (AWS ALB, NGINX, HAProxy) có idle timeout mặc định 60-300 giây. Nếu kết nối idle quá lâu, LB đóng kết nối phía server nhưng không thông báo cho client. Client tiếp tục giữ connection trong pool, gửi request → nhận RST.
Client Pool LB Server
|--ESTABLISHED---|--ESTABLISHED---|
| (idle 5 min) | |
| |--RST---------->| LB close server-side
| | |
|--request------>| |
|<--RST----------| LB tra RST vi con no phia client het
ECONNRESET
c) Vượt quá giới hạn. Server từ chối connection vì quá tải (backlog đầy), gửi RST thay vì SYN-ACK.
d) Đọc sau khi peer đã đóng socket. Ứng dụng gọi read() trên socket sau khi peer đã gửi FIN và ứng dụng đã gửi dữ liệu thêm — peer trả RST để báo lỗi trình tự.
Hay gặp nhất trong thực tế — connection pool giữ connection chết:
// Connection pool (HikariCP, Apache HttpClient...) giu ket noi "da chet"
// LB da close phia sau nhung pool chua biet
HttpGet request = new HttpGet("http://internal-service/api");
// get() lay connection tu pool -> connection bi RST boi LB
// -> SocketException: Connection reset
Chẩn đoán:
# Xem connection dang ESTABLISHED voi remote host
ss -tn dst 10.0.0.5
# Check LB idle timeout (AWS CLI)
aws elbv2 describe-load-balancer-attributes --load-balancer-arn <arn> \
| jq '.Attributes[] | select(.Key == "idle_timeout.timeout_seconds")'
# Tcpdump bat RST
sudo tcpdump -n 'tcp port 8080 and tcp[tcpflags] & tcp-rst != 0'
Fix: cấu hình connection pool gửi TCP keep-alive hoặc kiểm tra connection trước khi mượn, đảm bảo pool timeout ngắn hơn LB idle timeout. Xem thêm bài 06 — Connection pooling & keep-alive.
4. ETIMEDOUT — im lặng tuyệt đối
ETIMEDOUT là lỗi xảy ra khi TCP đã retransmit gói nhiều lần và vẫn không nhận được phản hồi. Kernel từ bỏ sau khoảng 1-3 phút (tùy OS config).
Cơ chế: TCP có cơ chế retransmission exponential backoff. Khi không nhận được ACK, TCP gửi lại gói với khoảng chờ tăng dần: 1s → 2s → 4s → 8s → ... Sau khi vượt quá tcp_syn_retries (mặc định 6 lần trên Linux, tổng ~127 giây), kernel trả ETIMEDOUT.
# Xem so lan retry SYN
cat /proc/sys/net/ipv4/tcp_syn_retries
# 6 (mac dinh) -- tong thoi gian cho ~127s
# Xem so lan retry du lieu da established
cat /proc/sys/net/ipv4/tcp_retries2
# 15 (mac dinh) -- tong thoi gian cho khoang 13-30 phut
Nguyên nhân phổ biến:
a) Firewall DROP (không REJECT). Firewall DROP nuốt gói im lặng — không gửi RST, không gửi ICMP "port unreachable". Client chờ mãi. Đây là hành vi mặc định của nhiều firewall và security group cloud (AWS Security Group mặc định DROP nếu không có rule allow).
b) Server quá tải, không kịp accept(). Socket listen backlog đầy — OS từ chối nhận SYN bằng cách im lặng (không gửi RST, không SYN-ACK). Client chờ retransmit. Xem output ss -tlnp — cột Recv-Q tăng cao là dấu hiệu.
c) Route hỏng. Gói đi ra nhưng không biết đi về đâu — black-hole routing.
d) PMTUD black-hole. Gói lớn hơn MTU bị drop âm thầm (xem bài 08 — MTU & fragmentation).
sequenceDiagram
participant C as Client
participant F as Firewall (DROP)
participant S as Server
C->>F: SYN (port 8080)
Note over F: DROP - khong gui gi ve
C->>F: SYN retry (1s)
C->>F: SYN retry (2s)
C->>F: SYN retry (4s)
Note over C: sau ~127s: ETIMEDOUTChẩn đoán:
# Thu connect -- neu loi den sau 30+ giay la ETIMEDOUT
time nc -zv -w 5 10.0.0.5 8080
# nc: connect to 10.0.0.5 port 8080 (tcp) timed out: Operation now in progress
# Kiem tra firewall rule (Linux iptables)
iptables -L -n --line-numbers | grep -E "DROP|REJECT"
# Xem backlog listen socket
ss -tlnp | grep :8080
# Recv-Q cao -> backlog day
# tcpdump de xac nhan goi SYN di ra nhung khong co gi ve
sudo tcpdump -n 'tcp port 8080'
# Chi thay SYN, khong co SYN-ACK -> packet bi nuot
5. EADDRINUSE và EPIPE
EADDRINUSE — port đang bị chiếm
EADDRINUSE xảy ra khi ứng dụng cố gắng bind() một port đang được dùng bởi:
- TIME_WAIT: kết nối cũ đang trong giai đoạn chờ 2×MSL (thường 60-120 giây). Đây là nguyên nhân phổ biến khi restart server sau khi có nhiều kết nối ngắn. Xem bài 04 — TIME_WAIT & hết port.
- Process khác: process khác (hoặc bản sao cũ chưa tắt hẳn) đang giữ port.
# Tim process dang giu port
ss -tlnp sport = :8080
# Hoac:
lsof -i :8080
# Fix TIME_WAIT voi SO_REUSEADDR (server code)
# Trong Java: serverSocket.setReuseAddress(true);
# -- cho phep bind lai port du co TIME_WAIT
# Voi SO_REUSEPORT (Linux 3.9+): nhieu process cung bind 1 port
EPIPE — ghi vào socket sau khi peer đóng
EPIPE xảy ra khi ứng dụng cố ghi vào socket sau khi peer đã đóng phía nhận của nó. OS gửi SIGPIPE (mặc định terminate process) và trả lỗi EPIPE nếu signal bị ignore.
# Sequence: peer send FIN -> ack -> app write -> RST -> EPIPE
# Trong Java: SocketException: Broken pipe
Fix: luôn kiểm tra trạng thái kết nối hoặc handle exception EPIPE/Broken pipe. Với server HTTP, middleware thường handle sẵn.
6. Bảng tra lỗi → nguyên nhân → công cụ
| Lỗi | Thời điểm xuất hiện | Nguyên nhân khả dĩ | Công cụ chẩn đoán |
|---|---|---|---|
ECONNREFUSED | Tức thì (dưới 1s) | Không có process listen, port sai, service chưa start | ss -tlnp, nc -zv, curl -v |
ECONNRESET | Bất kỳ lúc nào | Server crash, LB idle timeout, pool connection chết, vượt giới hạn | tcpdump rst, ss -tn, check LB log |
ETIMEDOUT | Sau 30-127s (SYN) hoặc phút (data) | Firewall DROP, route hỏng, backlog đầy, PMTUD black-hole | tcpdump, iptables -L, ss -tlnp Recv-Q |
EADDRINUSE | Khi bind port | TIME_WAIT, process khác giữ port | ss -tlnp, lsof -i :<port> |
EPIPE | Khi write | Peer đã close socket phía nhận | tcpdump, check application log |
flowchart TD
A["Loi mang"] --> B{"Toc do xuat hien?"}
B -->|"Tuc thi < 1s"| C["ECONNREFUSED<br/>khong ai listen"]
B -->|"Sau 30-127s"| D["ETIMEDOUT<br/>firewall DROP / route hong"]
B -->|"Bat ky luc nao"| E{"Co ESTABLISHED truoc?"}
E -->|"Co"| F["ECONNRESET<br/>server crash / LB timeout / pool chet"]
E -->|"Khong"| D7. Pitfall — nhầm lẫn thường gặp
❌ Nhầm 1: "Mình bị ECONNREFUSED, chắc firewall chặn rồi."
✅ Ngược lại. ECONNREFUSED có nghĩa là gói đã đến được OS phía server — firewall không chặn. Vấn đề là không có process nào listen ở port đó. Kiểm tra ss -tlnp để xác nhận.
❌ Nhầm 2: "ETIMEDOUT và ECONNREFUSED đều là không connect được, giống nhau thôi."
✅ Khác nhau hoàn toàn. ECONNREFUSED đến tức thì (có RST) → vấn đề tầng process. ETIMEDOUT đến sau phút (không có RST) → vấn đề tầng mạng/firewall. Hai hướng debug khác hẳn nhau.
❌ Nhầm 3: "Connection pool tự động phát hiện connection chết."
✅ Pool mặc định không tự kiểm tra. HikariCP cần cấu hình keepaliveTime hoặc connectionTestQuery. Nếu LB idle timeout ngắn hơn pool max-lifetime, pool sẽ cho mượn connection chết → ECONNRESET. Fix: đặt pool maxLifetime ngắn hơn LB idle timeout ít nhất 30 giây.
❌ Nhầm 4: "Ping được là network ổn, lỗi do code."
✅ ping dùng ICMP, không liên quan TCP. Firewall có thể allow ICMP nhưng DROP TCP port cụ thể. Ping thông mà TCP timeout là chuyện bình thường — test bằng nc -zv hoặc curl -v thay vì ping.
8. Liên hệ các bài khác
- Bài 01 — 3-way handshake:
ECONNREFUSEDxảy ra khi SYN không được đáp bằng SYN-ACK mà bằng RST.ETIMEDOUTkhi SYN không được đáp gì cả — kết nối kẹt ởSYN_SENT. - Bài 04 — TIME_WAIT & hết port:
EADDRINUSEthường xuất phát từ TIME_WAIT chiếm port khi server restart — bài 04 giải thích cơ chế và cách config SO_REUSEADDR. - Bài 06 — Connection pooling & keep-alive:
ECONNRESETtừ pool connection chết là vấn đề trực tiếp của bài 06 — pool cần biết khi nào connection idle quá lâu. - Bài 08 — MTU & fragmentation: PMTUD black-hole là một nguyên nhân
ETIMEDOUTđặc biệt — gói nhỏ (ping) qua được nhưng gói lớn (TLS handshake, HTTP response lớn) bị drop im lặng.
9. 📚 Deep Dive — tài liệu gốc
Đọc khi muốn đi tới tận gốc:
- RFC 9293 — Transmission Control Protocol — mục 3.5.2 mô tả chính xác khi nào TCP gửi RST, điều kiện ECONNREFUSED và ECONNRESET.
- POSIX
errno—ECONNREFUSED(111 trên Linux),ECONNRESET(104),ETIMEDOUT(110),EADDRINUSE(98),EPIPE(32). Traman errnohoặc/usr/include/asm-generic/errno.h. - Linux
tcp_syn_retriessysctl — kiểm soát thời gian trước khi ETIMEDOUT khi connect.
Ghi chú: RFC 9293 mục 3.5.2 "Reset Generation" mô tả đầy đủ các điều kiện kernel gửi RST — nguồn chính thức để phân biệt ECONNREFUSED vs ECONNRESET về mặt giao thức.
10. Tóm tắt
ECONNREFUSED— lỗi tức thì, server OS trảRSTngay vì không có socket LISTEN ở port đó. Gói đã đến được server — không phải vấn đề firewall.ECONNRESET— kết nối đang mở bị phá bởiRST: server crash, LB idle timeout, pool connection chết, hoặc vi phạm giao thức. Hay gặp nhất với pool không cấu hình keep-alive.ETIMEDOUT— gói bị nuốt im lặng: firewall DROP, route hỏng, backlog đầy. Đến sau 30-127 giây (SYN) hoặc nhiều phút (data). Ping thường vẫn thông vì ICMP không bị chặn.EADDRINUSE— port bị chiếm bởi TIME_WAIT hoặc process khác. Fix bằngSO_REUSEADDRhoặc chờ TIME_WAIT hết.EPIPE— ghi vào socket sau khi peer đã đóng. Thường gặp khi response trả lại sau khi client đã timeout và đóng kết nối.- Bộ tam chẩn đoán nhanh:
ss -tlnp(ai đang listen?),nc -zv(connect thử),tcpdump(gói RST có xuất hiện không?).
11. Tự kiểm tra
Q1Bạn gọi connect() tới một server và lỗi đến trong vòng 1ms. Đó là lỗi gì và nguyên nhân là gì?▸
RST ngay lập tức — cơ chế của kernel khi không có socket nào đang LISTEN ở port đó. Vì gói đã đến được server, firewall không phải nguyên nhân. Kiểm tra bằng ss -tlnp để xem process nào (nếu có) đang listen ở port đó.Q2Service của bạn dùng connection pool. Sau khi deploy xong khoảng 5 phút, bắt đầu có lỗi ECONNRESET lẻ tẻ. Nguyên nhân khả dĩ nhất là gì?▸
RST → ECONNRESET. Fix: cấu hình pool maxLifetime ngắn hơn LB idle timeout ít nhất 30 giây, hoặc bật keepaliveTime để pool tự kiểm tra. Xem bài 06 để hiểu cơ chế keep-alive.Q3Vì sao ETIMEDOUT xảy ra lâu hơn ECONNREFUSED rất nhiều (127 giây so với dưới 1ms)?▸
tcp_syn_retries), tổng cộng khoảng 127 giây mới từ bỏ và báo ETIMEDOUT. Thiết kế này để TCP robust với mất gói ngắn hạn.Q4Bạn ping tới một host thành công, nhưng khi curl HTTP tới cùng host đó thì timeout. Điều này có mâu thuẫn không? Giải thích.▸
ping dùng giao thức ICMP (tầng Internet), không dùng TCP. Firewall hoặc security group có thể cấu hình allow ICMP (để ping hoạt động bình thường) nhưng DROP TCP port 80/443. Hai giao thức độc lập nhau hoàn toàn — ping thành công không chứng minh được TCP sẽ thành công. Test TCP bằng nc -zv host 80 hoặc curl -v thay vì ping.Q5Server của bạn restart và ngay lập tức báo EADDRINUSE khi cố bind lại port 8080. Nguyên nhân là gì và fix thế nào?▸
SO_REUSEADDR (Java: serverSocket.setReuseAddress(true)) — cho phép bind lại port dù có TIME_WAIT. Fix căn gốc: tìm hiểu tại sao có nhiều TIME_WAIT — xem bài 04.Q6Vì sao ECONNRESET nguy hiểm hơn ECONNREFUSED từ góc độ data integrity?▸
ESTABLISHED, có thể đã gửi một phần request hoặc nhận được một phần response. Khi RST đến, buffer bị xóa ngay lập tức — dữ liệu đang gửi dở không được giao và không có cách biết server đã nhận được bao nhiêu. Với các thao tác không idempotent (POST, ghi database), ECONNRESET giữa chừng cần được xử lý cẩn thận để tránh trạng thái không nhất quán.Q7Bạn dùng tcpdump và thấy client liên tục gửi SYN nhưng không bao giờ nhận SYN-ACK. Điều này gợi ý gì về nơi sự cố đang xảy ra?▸
ss -tlnp Recv-Q phía server). Bước tiếp: chạy tcpdump ở phía server — nếu SYN không xuất hiện thì firewall đang DROP trước khi đến server.Bài tiếp theo: MTU & fragmentation — vì sao ping được mà app timeout
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên