Bài trước nhắc redirect tốn "một vòng khứ hồi". Bài này đào vào chính cái giá đó: mỗi khi client mở một kết nối mới tới server, có một chuỗi bắt tay xảy ra trước khi một byte dữ liệu nào được gửi. Với HTTP/1.0 nguyên thủy, mỗi request mở một kết nối rồi đóng — trả giá handshake mỗi lần. HTTP/1.1 sửa điều đó bằng keep-alive: giữ kết nối mở để tái dùng. Nghe nhỏ, nhưng đo thật cho thấy khác biệt tới gần 20 lần. Ta đo bằng curl 7.88 và một socket đếm kết nối.
Cái giá của một kết nối mới
Trước khi gửi request, một kết nối HTTPS mới phải qua:
- DNS: tra tên miền → IP (nếu chưa cache).
- Bắt tay TCP ba bước (SYN → SYN-ACK → ACK): tốn một vòng khứ hồi (RTT).
- Bắt tay TLS (nếu HTTPS): thêm 1–2 RTT nữa để thỏa thuận khóa (chi tiết ở bài TLS sau).
Chỉ sau toàn bộ chuỗi đó, request mới được gửi và byte dữ liệu đầu tiên mới về. Trên mạng có độ trễ, mỗi RTT là hàng chục tới hàng trăm mili-giây — nhân lên cho mỗi kết nối mới.
curl -o /dev/null -w \
"DNS=%{time_namelookup} TCP=%{time_connect} TLS=%{time_appconnect} first_byte=%{time_starttransfer}" URL
# time_connect - time_namelookup = chi phí TCP
# time_appconnect - time_connect = chi phí TLS

Hình 1: Chuỗi bắt tay của một kết nối mới (DNS → TCP → TLS) diễn ra trước mọi byte dữ liệu. keep-alive tái dùng kết nối để các request sau bỏ qua chuỗi này; curl -w cho đo từng mốc.
Đo thật: bắt tay tốn bao nhiêu
Gọi curl -w tới một host thật trên Internet để thấy từng mốc:

Hình 2: Handshake thật — TCP tốn ~27ms, TLS thêm ~31ms, tổng ~69ms trước byte đầu. 6 request keep-alive chỉ mở 1 kết nối TCP (so với 6 khi Connection: close). Và 300 request tái dùng nhanh gấp ~19 lần.
Ba lớp bằng chứng, đều đo thật:
- Handshake tốn thật: tới một host Internet,
time_connect(TCP) ≈ 38ms,time_appconnect(TLS xong) ≈ 69ms — nghĩa là ~69ms trôi qua trước khi request được gửi. Với mỗi kết nối mới, khoản này trả lại từ đầu. - keep-alive giảm số kết nối: một server đếm kết nối TCP cho thấy 6 request qua một lệnh curl (tái dùng) chỉ mở 1 kết nối; còn gửi 6 lần với
Connection: closemở 6 kết nối — tức 6 lần bắt tay. - Khác biệt thời gian rõ rệt: 300 request lên localhost, keep-alive mất 0.038s (1 kết nối), còn mở mới mỗi lần mất 0.737s (300 kết nối) — chậm gấp ~19 lần, ngay cả khi localhost gần như không có độ trễ mạng. Trên mạng thật, mỗi handshake là 1+ RTT nên khoảng cách còn lớn hơn nhiều.
Con số 19 lần trên localhost là chặn dưới — nó chỉ phản ánh chi phí tạo/hủy socket và syscall, chưa tính RTT. Đây là lý do mọi HTTP client nghiêm túc (thư viện, trình duyệt, reverse proxy) đều dùng connection pool: giữ sẵn vài kết nối mở để tái dùng thay vì mở mới liên tục.
keep-alive hoạt động thế nào
Trong HTTP/1.1, keep-alive là mặc định — kết nối được giữ mở sau mỗi response trừ khi có Connection: close. Client gửi request tiếp trên cùng socket, bỏ qua toàn bộ DNS+TCP+TLS. Server đóng kết nối khi rảnh quá lâu (keep-alive timeout, thường vài giây tới vài chục giây) hoặc khi đã phục vụ đủ số request cấu hình. Với curl, đưa nhiều URL cùng một host vào một lệnh là nó tự tái dùng socket; các thư viện thì dùng connection pool bên dưới.
Lưu ý: keep-alive của HTTP/1.1 vẫn tuần tự trên một kết nối — request sau phải chờ response trước xong (head-of-line blocking). Muốn nhiều request song song trên một kết nối phải tới HTTP/2 (bài sau).
Đánh đổi và lưu ý
Kết nối mở tốn tài nguyên server. Mỗi kết nối keep-alive giữ một socket và bộ nhớ ở server. Timeout quá dài + nhiều client nhàn rỗi = cạn socket. Đó là lý do server đặt keep-alive timeout hợp lý (nginx mặc định 75s cho client) và giới hạn số request mỗi kết nối. Cân bằng giữa tái dùng (nhanh) và giải phóng tài nguyên.
Không phải lúc nào cũng thắng. Một request đơn lẻ tới một host rồi thôi thì keep-alive không giúp gì (không có request sau để tái dùng). Lợi ích đến khi có nhiều request tới cùng host — đúng trường hợp phổ biến của trình duyệt tải một trang (HTML + CSS + JS + ảnh) hay client gọi API liên tục.
Đo bằng curl -w, đừng đoán. Các biến %{time_namelookup}, %{time_connect}, %{time_appconnect}, %{time_starttransfer}, %{time_total} tách bạch từng giai đoạn — cực hữu ích khi một request "chậm" mà không rõ chậm ở DNS, bắt tay, hay chờ server xử lý.
Ba ý mang về
- Mỗi kết nối mới trả giá handshake trước khi gửi dữ liệu: đo thật tới một host Internet, ~69ms trôi qua cho DNS+TCP+TLS trước byte đầu tiên — mở mới mỗi request là trả khoản này lặp lại.
- keep-alive tái dùng kết nối: đo thật, 6 request qua một socket keep-alive chỉ mở 1 kết nối TCP (so với 6 khi
Connection: close); 300 request tái dùng nhanh gấp ~19 lần ngay cả trên localhost — trên mạng thật còn lớn hơn. - Biết giới hạn: HTTP/1.1 keep-alive vẫn tuần tự (head-of-line blocking → cần HTTP/2 để song song); kết nối mở tốn tài nguyên server nên có timeout; và lợi ích chỉ đến khi có nhiều request tới cùng host — đo bằng
curl -wthay vì đoán.
Nguồn
- MDN — Connection management in HTTP/1.x (keep-alive, pipelining): https://developer.mozilla.org/en-US/docs/Web/HTTP/Connection_management_in_HTTP_1.x
- everything curl — Timing (các biến
-w): https://everything.curl.dev/usingcurl/verbose/writeout
Phần sau ta tiến thêm một bước: HTTP/2 giải quyết đúng giới hạn tuần tự vừa nhắc — multiplexing nhiều request song song trên một kết nối, và head-of-line blocking dịch chuyển đi đâu.