Một request HTTPS mất bao lâu: DNS, TCP, TLS và việc dùng lại kết nối
Tách một request HTTPS thành DNS, TCP, TLS và chờ phản hồi, đo từng pha bằng curl và Python, rồi tính cái giá của việc mở kết nối mới cho mỗi lời gọi.
Khách bấm "Thanh toán" trên app BanHang. App gửi request tới API, API gọi cổng thanh toán bên ngoài qua HTTPS. Trước khi cổng thanh toán đọc được byte đầu tiên của yêu cầu, hai bên đã đi về vài lượt chỉ để mở kết nối. Bài này tách một request HTTPS thành từng pha, đo từng pha trên một máy ở Việt Nam, rồi tính xem dùng lại kết nối tiết kiệm bao nhiêu cho lời gọi cổng thanh toán.
Đọc nhanh
- Kết nối mới qua TCP và TLS 1.3 tốn ít nhất 3 RTT trước byte đầu tiên của phản hồi: TCP 1, TLS 1, request 1. TLS 1.2 thêm 1 RTT, DNS thêm khi cache lạnh. Kết nối dùng lại chỉ tốn 1 RTT.
- Trên máy thử, RTT khoảng 50 ms: 20 request, mỗi request một kết nối mới, mất 4,4 s. Trên một kết nối dùng lại, cùng 20 request mất 1,5 s.
- Các mốc
-wcủacurltách được mạng và server: lấy thời gian từ lúc xong TLS đến byte đầu tiên, trừ một RTT, là thời gian nằm phía sau server. - Trong .NET, không tạo
HttpClientcho mỗi request. DùngIHttpClientFactoryhoặc mộtHttpClientsống lâu vớiPooledConnectionLifetime.
Máy thử
Laptop Windows 11 ở Việt Nam, Wi-Fi, resolver 8.8.8.8, đo ngày 2026-10-01. curl.exe 8.21.0 đi kèm Windows, TLS qua Schannel, không có HTTP/2 và HTTP/3, nên mọi phép đo curl là HTTP/1.1. Python 3.14.3 với OpenSSL 3.0.18. .NET SDK 10.0.401. Cloudflare phục vụ docs.gamevibe.win cho máy này từ điểm HKG. Số đo qua Wi-Fi dao động mạnh giữa các lần chạy và đổi theo vị trí của bạn.
1. Một request HTTPS gồm những pha nào
RTT (round-trip time) là thời gian một gói đi từ client tới server và nhận trả lời. Mỗi pha của một request trên kết nối mới tốn một số RTT nguyên, cộng thời gian tính toán ở hai đầu.
- DNS. Đổi tên miền thành địa chỉ IP. Tên đã nằm trong cache của hệ điều hành thì gần như miễn phí. Nếu chưa, client hỏi resolver, và resolver có thể phải hỏi tiếp máy chủ có thẩm quyền.
- TCP. Client gửi
SYN, server trảSYN-ACK. Sau 1 RTT client gửi được dữ liệu. - TLS. Ở TLS 1.3, client gửi
ClientHellokèm khóa tạm. Server trả toàn bộ phần của mình, từServerHellođếnFinished, trong một lượt (RFC 8446, mục 2). Client gửi được dữ liệu sau 1 RTT. RFC 7918 ghi rằng bắt tay đầy đủ ở các bản đến TLS 1.2 cần hai vòng, bốn chặng, trước khi gửi được dữ liệu ứng dụng. - Request và response. Ít nhất 1 RTT cộng thời gian server xử lý. Phản hồi lớn tốn thêm vài RTT vì TCP mở rộng cửa sổ gửi dần dần.
kết nối mới, TLS 1.3 ≈ 1 RTT (TCP) + 1 RTT (TLS) + 1 RTT (request) + thời gian server = 3 RTT + thời gian server
kết nối dùng lại ≈ 1 RTT (request) + thời gian server
HTTP/3 chạy trên QUIC. QUIC gộp bắt tay vận chuyển và bắt tay mật mã làm một (RFC 9000, mục 7), nên kết nối mới còn 2 RTT trước byte đầu tiên. Cách RFC 9114 (mục 3.1.1) mô tả để client biết server có HTTP/3 là header Alt-Svc trong một phản hồi trước đó. Vì vậy lần kết nối đầu tiên tới một server thường vẫn đi qua TCP.
TLS 1.3 còn có 0-RTT: khi client đã có khóa chung (PSK), thường lấy từ phiên trước, nó gửi dữ liệu ngay trong lượt đầu tiên.
0-RTT không dành cho lệnh thanh toán
RFC 8446 (mục 2.3) ghi rõ dữ liệu 0-RTT không có forward secrecy và không được đảm bảo chống phát lại giữa các kết nối. Ai chép được gói đó có thể gửi lại nguyên văn. Khi không có thông tin gì khác, RFC 8470 (mục 4) cấm client HTTP gửi method không an toàn như POST trong early data, và server có thể từ chối bằng mã 425 Too Early (mục 5.2). Lệnh trừ tiền luôn chờ bắt tay xong, và vẫn cần idempotency key.
2. Đo từng pha bằng curl
Tùy chọn -w của curl in các mốc thời gian, tính bằng giây, cộng dồn từ lúc bắt đầu: time_namelookup (xong DNS), time_connect (xong TCP), time_appconnect (xong TLS), time_starttransfer (byte đầu tiên của phản hồi tới), time_total (xong toàn bộ). Hiệu giữa hai mốc liền nhau là độ dài một pha.
Script PowerShell dưới đây đo ba URL, mỗi URL 15 lần, in trung vị. gamevibe là trang chủ blog này. wiki-cache là trang chủ Wikipedia, trả từ cache ở edge. wiki-api là một lời gọi API mà edge không cache (header x-cache-status: pass), nên phải chuyển tiếp về hệ thống phía sau. Trên Linux, một lần đo tương đương là curl -s -o /dev/null -w '...' URL.
$fmt = "%{time_namelookup} %{time_connect} %{time_appconnect} %{time_starttransfer} %{time_total}\n"
$urls = [ordered]@{
"gamevibe" = "https://docs.gamevibe.win/"
"wiki-cache" = "https://www.wikipedia.org/"
"wiki-api" = "https://vi.wikipedia.org/w/api.php?action=query&meta=siteinfo&format=json"
}
function Median($xs) { ($xs | Sort-Object)[[int](($xs.Count - 1) / 2)] }
foreach ($ten in $urls.Keys) {
$r = 1..15 | ForEach-Object {
$t = (curl.exe -s -o NUL -w $fmt $urls[$ten]) -split " " | ForEach-Object { 1000 * [double]$_ }
[pscustomobject]@{ dns = $t[0]; tcp = $t[1] - $t[0]; tls = $t[2] - $t[1]; cho = $t[3] - $t[2]; tong = $t[4] }
}
"{0,-10} dns {1,4:N0} tcp {2,4:N0} tls {3,4:N0} cho {4,4:N0} tong {5,4:N0} (ms, trung vi 15 lan)" -f $ten,
(Median $r.dns), (Median $r.tcp), (Median $r.tls), (Median $r.cho), (Median $r.tong)
}
gamevibe dns 21 tcp 49 tls 112 cho 95 tong 298 (ms, trung vi 15 lan)
wiki-cache dns 28 tcp 59 tls 135 cho 78 tong 434 (ms, trung vi 15 lan)
wiki-api dns 37 tcp 57 tls 140 cho 287 tong 530 (ms, trung vi 15 lan)
tcpgần bằng RTT: 49–59 ms.ping docs.gamevibe.wincho trung bình 47 ms.tlslà 112–140 ms, khoảng 2,3–2,5 RTT. Cùng serverdocs.gamevibe.win, Python với OpenSSL đo được 75 ms (mục 3). Đường truyền và server như nhau, chỉ thư viện TLS phía client khác, nên phần chênh nằm ở phía client. Dùng curl để so các server với nhau, không dùng con số tuyệt đối của nó để kết luận về TLS.cholà một RTT cộng thời gian phía sau server.wiki-cache: 78 − 59 = 19 ms.wiki-api: 287 − 57 = 230 ms. Hai URL đi qua cùng máy cachecp5024(headerx-cache), TCP và TLS như nhau. Khoảng 210 ms chênh lệch nằm hoàn toàn sau edge, nơi request API chờ hệ thống ứng dụng trả lời.tongtrừ bốn cột trước là thời gian tải thân phản hồi: 21 ms cho trang blog 15 KB, 134 ms cho trang Wikipedia 94 KB. 94 KB không đi hết trong một lượt TCP.
Hai công thức dùng khi điều tra:
RTT ≈ time_connect − time_namelookup
thời gian phía server ≈ (time_starttransfer − time_appconnect) − RTT
DNS cần phép thử riêng. Đoạn dưới gọi 11 tên miền Wikipedia chưa từng phân giải trên máy, mỗi tên hai lần liền nhau:
$lan1 = @(); $lan2 = @()
foreach ($l in "fi", "da", "cs", "hu", "ro", "tr", "id", "th", "uk", "el", "ms") {
$u = "https://$l.wikipedia.org/"
$lan1 += 1000 * [double](curl.exe -s -o NUL -w "%{time_namelookup}" $u) # ten chua co trong cache
$lan2 += 1000 * [double](curl.exe -s -o NUL -w "%{time_namelookup}" $u) # ngay sau do
}
"DNS lan 1: trung vi {0,4:N0} ms" -f ($lan1 | Sort-Object)[5]
"DNS lan 2: trung vi {0,4:N0} ms" -f ($lan2 | Sort-Object)[5]
DNS lan 1: trung vi 118 ms
DNS lan 2: trung vi 21 ms
Lần 1, Windows phải hỏi 8.8.8.8 (ping 43 ms). Lần 2 lấy từ cache. 21 ms còn lại là chi phí riêng của curl.exe trên máy này: socket.getaddrinfo của Python cho tên đã cache chỉ mất 1,2–1,3 ms. Thêm --resolve docs.gamevibe.win:443:<IP> thì time_namelookup xuống dưới 0,1 ms.
3. Kết nối mới và kết nối dùng lại
Script dưới đây làm hai việc. Phần 1 chỉ mở kết nối, đo riêng TCP và bắt tay TLS 1.3, TLS 1.2. Phần 2 gửi 20 request GET / tuần tự, hoặc đóng kết nối sau mỗi request, hoặc giữ một kết nối cho cả 20. HTTP/1.1 mặc định giữ kết nối mở giữa các request (RFC 9112, mục 9.3).
import http.client
import socket
import ssl
import statistics
import time
HOST, N = "docs.gamevibe.win", 20
DIA_CHI = socket.getaddrinfo(HOST, 443, socket.AF_INET, socket.SOCK_STREAM)[0][4]
def ms(t0):
return (time.perf_counter() - t0) * 1000
# Phần 1: chỉ mở kết nối, TLS 1.3 rồi TLS 1.2, mỗi loại 15 lần
for v in (ssl.TLSVersion.TLSv1_3, ssl.TLSVersion.TLSv1_2):
ctx = ssl.create_default_context()
ctx.minimum_version = ctx.maximum_version = v
tcp, tls = [], []
for _ in range(15):
t0 = time.perf_counter()
sock = socket.create_connection(DIA_CHI)
tcp.append(ms(t0))
t0 = time.perf_counter()
ctx.wrap_socket(sock, server_hostname=HOST).close() # trả về khi bắt tay xong
tls.append(ms(t0))
print(f"{v.name}: tcp {statistics.median(tcp):5.1f} ms, tls {statistics.median(tls):6.1f} ms")
# Phần 2: 20 request GET / tuần tự, đóng kết nối sau mỗi request hoặc giữ lại
CTX = ssl.create_default_context()
def chay(dung_lai):
t0 = time.perf_counter()
conn = http.client.HTTPSConnection(HOST, context=CTX)
for _ in range(N):
conn.request("GET", "/") # sau close(), http.client tự mở kết nối mới
conn.getresponse().read() # đọc hết body mới gửi tiếp được
if not dung_lai:
conn.close()
conn.close()
return ms(t0)
do = {False: [], True: []}
for _ in range(5): # xen kẽ hai cách, 5 vòng, lấy trung vị
for dung_lai in (False, True):
do[dung_lai].append(chay(dung_lai))
for dung_lai, ten in ((False, "kết nối mới mỗi lần"), (True, "một kết nối dùng lại")):
t = statistics.median(do[dung_lai])
print(f"{N} request, {ten}: {t:5.0f} ms ({t / N:5.1f} ms/request)")
TLSv1_3: tcp 49.0 ms, tls 75.0 ms
TLSv1_2: tcp 50.0 ms, tls 127.3 ms
20 request, kết nối mới mỗi lần: 4431 ms (221.6 ms/request)
20 request, một kết nối dùng lại: 1528 ms ( 76.4 ms/request)
TLS 1.2 chậm hơn TLS 1.3 khoảng 52 ms, xấp xỉ một RTT, đúng lượt đi về mà TLS 1.3 bỏ đi. Cả hai đều vượt số RTT nguyên khoảng 26–27 ms. Đó là thời gian tính toán ở hai đầu (trao đổi khóa, chữ ký, xác minh chuỗi chứng chỉ) cộng độ dao động của Wi-Fi.
Request trên kết nối dùng lại mất 76,4 ms: một RTT, thời gian edge trả trang, và thời gian tải 15 KB. Request trên kết nối mới mất thêm 145,2 ms. TCP và TLS 1.3 chiếm 49,0 + 75,0 = 124 ms trong đó. Phần còn lại gồm DNS từ cache, việc dựng và đóng socket, và dao động giữa các lần đo. Qua bốn lần chạy script, mức tiết kiệm mỗi request là 134–162 ms và TLS 1.2 chậm hơn TLS 1.3 từ 52 đến 68 ms.
4. Tính cho BanHang: lời gọi cổng thanh toán
Giả định API BanHang chạy ở Hà Nội, cổng thanh toán đặt server ở Singapore. RTT 30 ms giữa hai nơi là giá trị ví dụ, không phải số đo. Thời gian xử lý của cổng như nhau ở mọi trường hợp nên để riêng ra.
kết nối mới, TLS 1.3: TCP 1 + TLS 1 + request 1 = 3 RTT = 3 × 30 = 90 ms
kết nối mới, TLS 1.2: TCP 1 + TLS 2 + request 1 = 4 RTT = 4 × 30 = 120 ms
kết nối dùng lại: request 1 = 1 RTT = 30 ms
Dùng lại kết nối bớt 60 ms mỗi lời gọi so với TLS 1.3, 90 ms so với TLS 1.2. Khách thấy trực tiếp con số này ở màn hình thanh toán.
Lúc cao điểm API nhận khoảng 300 request mỗi giây. Lấy trần trên: mỗi request gọi ra ngoài qua HTTPS một lần. Thực tế lời gọi cổng thanh toán ít hơn nhiều, vì BanHang có khoảng 13.000 đơn mỗi ngày. Ước lượng khi mở kết nối mới cho mỗi lời gọi:
- 300 lần bắt tay TCP và TLS mỗi giây, ở cả API lẫn cổng thanh toán. Với pool dùng lại, số lần bắt tay chỉ bằng số kết nối trong pool, mỗi
PooledConnectionLifetimemột lần. - Theo định luật Little, số việc đang dở bằng tốc độ đến nhân thời gian mỗi việc: lúc nào cũng có 300 × 0,06 s = 18 lời gọi chỉ đứng chờ bắt tay.
- Bên chủ động đóng kết nối giữ socket ở trạng thái
TIME_WAITthêm một lúc. Trên máy thử là khoảng 120 giây (mục 5), và dải cổng động của Windows có 16.384 cổng (netsh int ipv4 show dynamicport tcp). 300 kết nối mỗi giây cần giữ 300 × 120 = 36.000 cổng cùng lúc, hơn gấp đôi số cổng có. Tới một địa chỉ IP của cổng thanh toán, cổng nguồn cạn sau khoảng 16.384 / 300 ≈ 55 giây. Khi hết cổng, lời gọi mới lỗi ngay ở bước mở kết nối.
Chặng từ app di động tới API khó giữ kết nối lâu như vậy: khi máy đổi từ Wi-Fi sang 4G, địa chỉ IP đổi và kết nối TCP cũ không dùng được nữa. Mỗi lần mở lại tốn DNS cộng 3 RTT, và HTTP/3 bớt được 1 RTT ở đây.
5. HttpClient mới cho mỗi request
Tài liệu .NET nêu cơ chế: mỗi HttpClient tự tạo handler thì có connection pool riêng, request tới cùng server qua handler khác phải mở kết nối mới, và cổng TCP không được trả lại ngay khi kết nối đóng. Đoạn dưới đo lỗi đó. Lưu thành KetNoi.cs, chạy bằng dotnet run KetNoi.cs (file-based app, cần .NET SDK 10).
using System.Diagnostics;
const string Url = "https://docs.gamevibe.win/";
const int N = 20;
using (var mo = new HttpClient()) await mo.GetStringAsync(Url); // làm nóng JIT, không tính giờ
var sw = Stopwatch.StartNew();
for (int i = 0; i < N; i++)
{
using var client = new HttpClient(); // SAI: mỗi HttpClient một connection pool riêng
using var res = await client.GetAsync(Url);
}
Console.WriteLine($"HttpClient mới mỗi request: {sw.ElapsedMilliseconds,5} ms");
var dungChung = new HttpClient(new SocketsHttpHandler { PooledConnectionLifetime = TimeSpan.FromMinutes(2) });
sw.Restart();
for (int i = 0; i < N; i++) { using var res = await dungChung.GetAsync(Url); }
Console.WriteLine($"Một HttpClient dùng chung: {sw.ElapsedMilliseconds,5} ms");
Chạy, rồi đếm socket TIME_WAIT tới địa chỉ của docs.gamevibe.win. Trước khi chạy, số này là 0.
$ips = (Resolve-DnsName docs.gamevibe.win -Type A).IPAddress
dotnet run KetNoi.cs
@(Get-NetTCPConnection -State TimeWait -RemotePort 443 | Where-Object { $ips -contains $_.RemoteAddress }).Count
HttpClient mới mỗi request: 4281 ms
Một HttpClient dùng chung: 1542 ms
21
21 socket ứng với 21 kết nối đã đóng: 20 trong vòng lặp sai, 1 của lần làm nóng. Vòng dùng chung không đóng kết nối nào. Ở một lần chạy khác, đếm lại mỗi 10 giây, 21 socket biến mất sau khoảng 120 giây. Qua bảy lần chạy, cách sai mất 4,3–10,1 s, cách đúng 1,4–3,2 s.
BanHang đăng ký client cho cổng thanh toán một lần, theo cách tài liệu IHttpClientFactory hướng dẫn khi dùng chung với SocketsHttpHandler. Đoạn dưới trích từ Program.cs, đã biên dịch và chạy trong một app thử với .NET SDK 10.0.401.
builder.Services.AddHttpClient<CongThanhToanClient>(c =>
{
c.BaseAddress = new Uri("https://cong-thanh-toan.example/");
c.Timeout = TimeSpan.FromSeconds(10);
})
.ConfigurePrimaryHttpMessageHandler(() => new SocketsHttpHandler
{
PooledConnectionLifetime = TimeSpan.FromMinutes(2) // đóng kết nối sau 2 phút để lần sau phân giải DNS lại
})
.SetHandlerLifetime(Timeout.InfiniteTimeSpan); // việc thay kết nối đã do PooledConnectionLifetime lo
public sealed class CongThanhToanClient(HttpClient http)
{
public Task<HttpResponseMessage> TaoGiaoDichAsync(object yeuCau, CancellationToken ct) =>
http.PostAsJsonAsync("v1/giao-dich", yeuCau, ct);
}
HttpClient chỉ phân giải DNS khi tạo kết nối và không theo TTL của bản ghi. Không có PooledConnectionLifetime, một kết nối sống mãi sẽ bám IP cũ khi cổng thanh toán đổi IP. Mốc 2 phút là giá trị ví dụ của tài liệu Microsoft, nên chọn theo tần suất DNS đổi. Tài liệu cũng cảnh báo không giữ typed client trong singleton, trừ khi handler là SocketsHttpHandler có PooledConnectionLifetime như trên.
6. Khi request chậm: thời gian nằm ở đâu
DNS. Resolve-DnsName docs.gamevibe.win cho TTL 300 giây. Mỗi lần bản ghi hết hạn, request kế tiếp trả thêm một lần hỏi resolver, ở máy thử khoảng 118 ms. Resolver xa hoặc TTL vài giây biến chi phí đó thành thường xuyên.
Chuỗi chứng chỉ. Server gửi chuỗi chứng chỉ trong lượt TLS đầu tiên. Từ Python 3.13 đo được cỡ chuỗi:
import socket
import ssl
ctx = ssl.create_default_context()
for host in ["docs.gamevibe.win", "github.com", "www.microsoft.com"]:
with ctx.wrap_socket(socket.create_connection((host, 443)), server_hostname=host) as s:
chuoi = s.get_unverified_chain() # Python 3.13+: các chứng chỉ server gửi, dạng DER
print(f"{host}: {len(chuoi)} chứng chỉ, {sum(map(len, chuoi))} byte")
docs.gamevibe.win: 3 chứng chỉ, 2492 byte
github.com: 3 chứng chỉ, 2718 byte
www.microsoft.com: 3 chứng chỉ, 5879 byte
Với TCP, cả ba chuỗi nằm gọn trong cửa sổ gửi ban đầu mà RFC 6928 cho phép, tối đa min(10 × MSS, max(2 × MSS, 14.600 byte)). Chuỗi vượt mức đó buộc server chờ ACK, tốn thêm một RTT. QUIC chặt hơn: trước khi xác thực địa chỉ client, server chỉ được gửi tối đa gấp ba số byte đã nhận (RFC 9000, mục 8.1). Gói Initial của client tối thiểu 1.200 byte (mục 14.1). Nếu client chỉ gửi một gói như vậy, server có khoảng 3 × 1.200 = 3.600 byte cho lượt đầu, và chuỗi 5.879 byte phải chờ client gửi thêm.
Nagle và delayed ACK. Nagle (RFC 1122, mục 4.2.3.4) giữ gói nhỏ lại khi còn dữ liệu chưa được ACK. Đầu kia được hoãn ACK tối đa 0,5 giây (mục 4.2.3.2). Hai cơ chế gặp nhau thì request nhỏ có thể chờ vô ích. Cả hai client trong bài đều tắt Nagle: http.client đặt TCP_NODELAY trong HTTPConnection.connect, SocketsHttpHandler tạo socket với NoDelay = true trong ConnectToTcpHostAsync. Code tự viết trên socket thô thì cần kiểm tra.
Danh sách kiểm tra khi lời gọi cổng thanh toán chậm:
- Chạy
curl -w10–15 lần từ chính máy API, lấy trung vị. Sotcpvớiping: cả hai lớn thì vấn đề nằm ở đường mạng hoặc khoảng cách. dnsvẫn lớn ở các lần lặp lại thì cache không giữ được tên: TTL quá ngắn hoặc resolver chậm.tlsvượt 2 RTT thì xem phiên bản TLS, cỡ chuỗi chứng chỉ, CPU ở hai đầu.chotrừ RTT lớn thì gửi con số đó, kèm thời điểm đo, cho bên vận hành cổng thanh toán.- Đếm socket
TIME_WAITtới địa chỉ cổng thanh toán (Get-NetTCPConnectiontrên Windows,ss -tan state time-waittrên Linux). Con số tăng theo lưu lượng nghĩa là kết nối không được dùng lại.
Những chỗ hay hiểu sai
- "HTTPS chậm vì mã hóa tốn CPU." Trên máy thử, bắt tay TLS 1.3 chỉ dài hơn một RTT khoảng 26 ms. Phần lớn chi phí là các lượt đi về, và dùng lại kết nối bỏ được chúng.
- "Dispose
HttpClientsau mỗi request là dọn dẹp đúng cách." Dispose đóng luôn connection pool. Đoạn đo ở mục 5 để lại 21 socketTIME_WAIT, mỗi socket giữ một cổng khoảng 2 phút. - "Một
HttpClientstatic là đủ." Nó không phân giải DNS lại khi kết nối còn sống. Cần đặtPooledConnectionLifetime. - "
time_starttransfercao nghĩa là server chậm." Mốc đó cộng dồn cả DNS, TCP và TLS. Phải trừtime_appconnectvà một RTT.
Đọc tiếp
- Idempotency key: để một đơn không bị trừ tiền hai lần: làm gì khi lời gọi cổng thanh toán timeout giữa chừng.
- Bài toán hai vị tướng và giới hạn của "gửi đúng một lần": vì sao không giao thức nào đảm bảo cả hai bên cùng biết một lệnh đã xong.
- Điều tra một truy vấn chậm từ cảnh báo đến bản sửa: cùng cách đo từng pha trước khi sửa, áp dụng cho SQL Server.
Nguồn
- RFC 8446, TLS 1.3, mục 2 và 2.3: https://www.rfc-editor.org/rfc/rfc8446
- RFC 7918, TLS False Start (số vòng bắt tay đầy đủ đến TLS 1.2): https://www.rfc-editor.org/rfc/rfc7918
- RFC 8470, Using Early Data in HTTP, mục 4 và 5.2: https://www.rfc-editor.org/rfc/rfc8470
- RFC 9000, QUIC, mục 7, 8.1 và 14.1: https://www.rfc-editor.org/rfc/rfc9000
- RFC 9114, HTTP/3, mục 3.1.1: https://www.rfc-editor.org/rfc/rfc9114
- RFC 9112, HTTP/1.1, mục 9.3: https://www.rfc-editor.org/rfc/rfc9112
- RFC 6928, Increasing TCP's Initial Window: https://www.rfc-editor.org/rfc/rfc6928
- RFC 1122, Requirements for Internet Hosts, mục 4.2.3.2 và 4.2.3.4: https://www.rfc-editor.org/rfc/rfc1122
- everything curl, danh sách biến
--write-out: https://everything.curl.dev/usingcurl/verbose/all-variables.html - Microsoft Learn, HttpClient guidelines for .NET: https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines
- Microsoft Learn, Use the IHttpClientFactory: https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory
- dotnet/runtime,
HttpConnectionPool.cs(nhánh main): https://github.com/dotnet/runtime/blob/main/src/libraries/System.Net.Http/src/System/Net/Http/SocketsHttpHandler/ConnectionPool/HttpConnectionPool.cs - CPython 3.14.3,
Lib/http/client.py, đọc từ bản cài trên máy thử - Tài liệu Python, module
ssl(get_unverified_chain, từ 3.13): https://docs.python.org/3/library/ssl.html