TCP, HTTP & Web cho Backend/ECONNREFUSED, ECONNRESET, ETIMEDOUT — đọc đúng nguyên nhân
8/29
Bài 8 / 29~22 phútTCP & UDP Deep DiveMiễn phí lượt xem

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:

  1. 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.
  2. 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.
  3. 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ườngLỗi mạng
Số không tồn tại → tổng đài báo ngayECONNREFUSED — server gửi RST tức thì
Đang nói chuyện bị cúp máy đột ngộtECONNRESET — peer gửi RST giữa chừng
Thư gửi đi nhưng không có hồi âmETIMEDOUT — gói bị DROP im lặng
Số điện thoại đang bậnEADDRINUSE — port bị chiếm
Ghi vào máy fax đã tắtEPIPE — ghi vào socket peer đã đóng
💡 Cách nhớ nhanh

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'
⚠️ ECONNREFUSED != firewall

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: ETIMEDOUT

Chẩ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ỗiThời điểm xuất hiệnNguyên nhân khả dĩCông cụ chẩn đoán
ECONNREFUSEDTức thì (dưới 1s)Không có process listen, port sai, service chưa startss -tlnp, nc -zv, curl -v
ECONNRESETBất kỳ lúc nàoServer crash, LB idle timeout, pool connection chết, vượt giới hạntcpdump rst, ss -tn, check LB log
ETIMEDOUTSau 30-127s (SYN) hoặc phút (data)Firewall DROP, route hỏng, backlog đầy, PMTUD black-holetcpdump, iptables -L, ss -tlnp Recv-Q
EADDRINUSEKhi bind portTIME_WAIT, process khác giữ portss -tlnp, lsof -i :<port>
EPIPEKhi writePeer đã close socket phía nhậntcpdump, 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"| D

7. 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: ECONNREFUSED xảy ra khi SYN không được đáp bằng SYN-ACK mà bằng RST. ETIMEDOUT khi SYN không được đáp gì cả — kết nối kẹt ở SYN_SENT.
  • Bài 04 — TIME_WAIT & hết port: EADDRINUSE thườ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: ECONNRESET từ 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

📚 RFC & tài liệu tham khảo

Đọ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 errnoECONNREFUSED (111 trên Linux), ECONNRESET (104), ETIMEDOUT (110), EADDRINUSE (98), EPIPE (32). Tra man errno hoặc /usr/include/asm-generic/errno.h.
  • Linux tcp_syn_retries sysctl — 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ả RST ngay 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ởi RST: 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ằng SO_REUSEADDR hoặ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

Tự kiểm tra
Q1
Bạ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ì?
Lỗi là ECONNREFUSED. Lỗi tức thì nghĩa là server OS đã nhận được gói SYN và trả về gói 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 đó.
Q2
Service 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ì?
Rất có thể pool đang giữ connection chết do Load Balancer đã đóng phía server sau idle timeout. Connection pool nhận request mới, lấy connection tưởng còn sống ra dùng, nhưng LB đã gửi RSTECONNRESET. 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.
Q3
Vì sao ETIMEDOUT xảy ra lâu hơn ECONNREFUSED rất nhiều (127 giây so với dưới 1ms)?
Với ECONNREFUSED, server OS nhận SYN và trả RST ngay — chỉ tốn 1 RTT. Với ETIMEDOUT, không có phản hồi nào về cả (firewall DROP, route hỏng). TCP không biết gói bị mất hay chỉ chậm, nên thực hiện exponential backoff retransmission: gửi lại SYN sau 1s, 2s, 4s, 8s, 16s, 32s (6 lần theo 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.
Q4
Bạ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.
Q5
Server 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?
Có hai nguyên nhân chính: (1) TIME_WAIT — các kết nối cũ chưa hết thời gian 2×MSL (thường 60-120 giây), OS vẫn giữ port để đảm bảo ACK cuối không bị mất. (2) Process cũ chưa tắt hẳn vẫn đang giữ socket. Fix ngắn hạn: đặt 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.
Q6
Vì sao ECONNRESET nguy hiểm hơn ECONNREFUSED từ góc độ data integrity?
ECONNREFUSED xảy ra trước khi kết nối được thiết lập — không có byte dữ liệu nào được gửi, nên không có rủi ro mất data. ECONNRESET xảy ra giữa chừng — kết nối đang 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.
Q7
Bạ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?
SYN lặp lại không có SYN-ACK là dấu hiệu của ETIMEDOUT. Gói đi ra (bạn thấy trong tcpdump phía client) nhưng không có phản hồi về. Ba khả năng: (1) firewall/security group DROP gói trước khi đến server; (2) route hỏng — gói đi ra nhưng không biết về đâu; (3) server nhận được nhưng backlog listen đầy nên im lặng (kiểm tra 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

Đặt 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

Bài tiếp theo

MTU & fragmentation — vì sao ping được mà app timeout