Skip to content

Split View: TCP 네트워크 스택 완전 정복 — 상태 머신, 혼잡 제어 (Cubic vs BBR), Nagle, Delayed ACK, 그리고 QUIC로의 진화 (2025)

|

TCP 네트워크 스택 완전 정복 — 상태 머신, 혼잡 제어 (Cubic vs BBR), Nagle, Delayed ACK, 그리고 QUIC로의 진화 (2025)

0. 의외로 우리가 모르는 TCP

매일 쓰는 HTTP 요청 뒤에는 항상 TCP 가 있다. 그런데 다음 질문들을 던져보자:

  • ss -tan 에서 본 TIME_WAIT 상태는 왜 30초나 있어야 하는가?
  • "connection reset by peer" 와 "broken pipe" 는 뭐가 다른가?
  • 초당 1KB 메시지 100개 보내기가 왜 4초나 걸릴 수 있는가?
  • 10Gbps 네트워크인데 iperf 가 1Gbps 밖에 안 나오는 이유가 뭔가?
  • 구글이 왜 TCP 대신 UDP 위에 QUIC 를 새로 만들었는가?

이 질문들의 답이 이 글에 있다. TCP 의 상태 머신, 혼잡 제어 알고리즘의 진화, 악명 높은 상호작용 버그들, 그리고 QUIC 시대의 등장까지.

1. TCP의 상태 머신 — 11개의 상태를 오가는 여정

1.1 연결 수립: 3-Way Handshake

Client              Server
  |  SYN (seq=x)      |
  |────────────────→ |  [LISTENSYN_RCVD]
  |                  |
  | SYN+ACK(seq=y,   |
  |  ack=x+1)        |
  | ←────────────────|
  |                  |
  |  ACK (ack=y+1)   |
  |────────────────→ |  [SYN_RCVDESTABLISHED]
  |                  |
  [SYN_SENT   ESTABLISHED]

질문: 왜 3번 인가? 2번이면 안 되나?

이유: 양방향 시퀀스 번호 동기화. 각 방향마다 독립적인 ISN (Initial Sequence Number) 을 상대에게 알리고 확인받아야 함. 클라이언트의 SYN/ACK는 자기 ISN+상대 ISN 확인, 클라이언트의 최종 ACK 는 서버 ISN 확인.

1.2 연결 종료: 4-Way Handshake

A                B
 |   FIN        |
 |─────────────→|  [ESTABLISHEDCLOSE_WAIT]
 |   ACK        |
 |←─────────────|
 |              |
 [FIN_WAIT_1
FIN_WAIT_2]
 |              |
 |   FIN        |
 |←─────────────|  [CLOSE_WAITLAST_ACK]
 |   ACK        |
 |─────────────→|
 |              |
 [FIN_WAIT_2
TIME_WAIT]    [LAST_ACKCLOSED]

4번 인가? TCP 는 전이중 (full-duplex). 각 방향을 독립적으로 닫는다. A 가 "나는 더 안 보낼게" 라고 해도 B 는 계속 보낼 수 있다.

1.3 TIME_WAIT — 오해 많은 상태

2MSL (Maximum Segment Lifetime) 동안 대기 — 보통 30초~2분.

왜?

  1. 지연 패킷이 새 연결에 섞이는 것 방지: 같은 포트 쌍으로 빠르게 새 연결을 만들면, 이전 연결의 지연 패킷이 새 연결에 도달할 수 있음. 2MSL 은 그 패킷들이 모두 소멸될 시간.
  2. 마지막 ACK 손실 대비: 상대가 FIN 을 재전송하면 ACK 를 다시 보내야 함. CLOSED 상태였다면 RST 를 보내서 상대 입장에서는 에러로 보임.

1.4 TIME_WAIT 폭발의 함정

클라이언트가 많은 연결을 빠르게 만들고 닫으면 TIME_WAIT 가 수만 개 쌓임. ephemeral port 고갈 → 새 연결 실패.

잘못된 해결: TIME_WAIT 를 0 으로 (위험, 데이터 오염 가능).

올바른 해결:

  • net.ipv4.tcp_tw_reuse=1: ephemeral port 를 TIME_WAIT 중이라도 재사용 (안전한 조건 하에).
  • Keep-Alive 연결 사용: HTTP/1.1 의 persistent, HTTP/2 의 multiplexing.
  • Connection pool: DB 드라이버, HTTP client.

1.5 CLOSE_WAIT — 앱 버그의 징후

TIME_WAIT 는 정상이지만, CLOSE_WAIT 가 쌓이면 앱 버그. 상대가 FIN 을 보냈는데 내 앱이 close() 를 안 호출한 상태.

$ ss -tan | grep CLOSE_WAIT | wc -l
50000   # 앱이 소켓 누수 중

원인: 예외 처리 없는 파일 디스크립터 관리, try-finally 누락.

1.6 TCP 상태 다이어그램 전체

         CLOSED (passive open)LISTEN
         │                          │
         (active open)         ▼                          │
      SYN_SENT         │                          │
              (SYN received)      SYN_RCVD ←─────────────── SYN_RCVD
         │                          │
         (ACK received)     (ACK)         ▼                          ▼
    ESTABLISHED ────────────→ ESTABLISHED
         │                          │
         (close)            (FIN)         ▼                          ▼
      FIN_WAIT_1                CLOSE_WAIT
         │                          │
         ▼                          ▼
      FIN_WAIT_2                LAST_ACK
         │                          │
         ▼                          ▼
      TIME_WAIT                   CLOSED
          (2MSL 후)
      CLOSED

2. TCP 의 신뢰성 메커니즘

2.1 시퀀스 번호와 ACK

모든 바이트에 시퀀스 번호. 수신자는 "다음에 받을 바이트 번호" 를 ACK 로 보냄.

송신: [1000][1001][1002][1003] ...
수신: ACK=1004  (1000~1003 까지 받음, 다음은 1004 기대)

중간에 손실되면 수신자는 같은 ACK 반복 (duplicate ACK). 송신자는 3개의 dup ACK 을 보면 Fast Retransmit 실행.

2.2 재전송 — RTO 와 Fast Retransmit

RTO (Retransmission TimeOut): 특정 시간 동안 ACK 없으면 재전송. RTT 측정으로 동적 조정 (Jacobson 알고리즘, 1988).

Fast Retransmit: 3 Dup ACK 받으면 타임아웃 안 기다리고 바로 재전송. 대부분의 손실을 수 ms 내 복구.

SACK (Selective ACK): 일반 ACK 는 연속 바이트만 표현. SACK 로 "10002000 받았고, 30004000 도 받음, 2000~3000 은 손실" 을 명시 → 정확한 재전송.

2.3 흐름 제어 — Receive Window

수신자 버퍼가 가득 차면 곤란. ACK 에 "내 수신 버퍼 여유 공간 (rwnd)" 을 같이 보냄.

송신자는 unacked_bytes < rwnd 인 동안만 전송. 수신자가 처리 못 따라가면 rwnd 가 줄어듦 → 송신 멈춤.

2.4 Window Scaling — 고대역폭 대응

TCP 헤더의 window 필드는 16비트 = 최대 65,535 바이트. 10Gbps × 100ms RTT = 125MB 필요 → 65KB 로는 턱없이 부족.

TCP Window Scaling (RFC 1323, 1992): SYN 시 "shift count" 를 교환. rwnd 를 2^shift 배 확대.

sysctl net.ipv4.tcp_window_scaling   # 1 (켜져 있음)
sysctl net.core.rmem_max              # 최대 수신 버퍼
sysctl net.ipv4.tcp_rmem              # 자동 조정 범위

3. 혼잡 제어 — 네트워크 공유의 예의범절

3.1 문제: 1986년의 Congestion Collapse

1986년 인터넷 사용량 폭증으로 UC Berkeley ↔ LBL 링크 (정상 32Kbps) 가 40bps 로 떨어짐. 1,000배 저하. 원인: 재전송 폭주 → 더 많은 손실 → 더 많은 재전송.

Van Jacobson 의 1988년 논문 "Congestion Avoidance and Control" 이 TCP 에 혼잡 제어를 도입.

3.2 Congestion Window (cwnd)

송신자는 네트워크 상태를 추정 하며 보낼 양을 제한:

전송 가능 = min(rwnd, cwnd)
  • rwnd: 수신자가 알려줌.
  • cwnd: 송신자가 추정 (네트워크 혼잡 피드백 기반).

3.3 Slow Start — 느리게 시작해서 기하급수로

초기 cwnd = 작은 값 (보통 10 MSS).

ACK 받을 때마다 cwnd += 1 MSS → RTT 한 번에 cwnd 가 2배.

cwnd: 10204080160...

이름이 "slow" 인 이유는 당시의 초기 cwnd=1 과 비교했을 때. 실제로는 지수 성장.

3.4 Congestion Avoidance — 선형 증가

ssthresh 도달 후 cwnd += 1 MSS per RTT → 선형 증가.

cwnd: 160161162...

3.5 손실 감지 시

  • 3 Dup ACK (Fast Retransmit): cwnd 절반으로. 온건한 감소.
  • Timeout: 심각한 문제로 판단. cwnd = 1 MSS 로 리셋, Slow Start 다시.

이 AIMD (Additive Increase, Multiplicative Decrease) 가 Reno 의 핵심.

3.6 TCP Reno → Cubic — 고대역폭 시대

고속 장거리 링크에서 Reno 는 너무 보수적. 1% 손실률에서 10Gbps 를 채우려면 수 분 걸림.

TCP BIC → CUBIC (Linux 2.6+ 기본): cwnd 를 시간의 3차 함수로 증가. 최근 손실 시점 기억하고 그 지점까지 빠르게 복귀 → 이후 천천히 탐색.

3.7 BBR — 구글의 혁명 (2016)

기존 알고리즘들의 공통 가정: "손실 = 혼잡". 하지만 현대 인터넷은 WiFi 노이즈, 무선 재전송, 버퍼블로트 등 비혼잡 손실이 많음.

BBR (Bottleneck Bandwidth and RTT) 의 아이디어:

"손실이 아니라 실제 대역폭과 RTT 를 직접 측정해서 cwnd 를 정하자."

  • 주기적으로 송신률을 늘려 현재 대역폭 탐색.
  • 최소 RTT 기억 (버퍼블로트 감지).
  • cwnd = BW × RTT 에 가깝게 유지 → 큐잉 최소화.

3.8 BBR 의 결과

구글 google.com, YouTube 가 BBR 전환 후:

  • YouTube 재버퍼링 4% 감소.
  • Google.com 응답 시간 감소.
  • 개발도상국 사용자에게 특히 효과.

Linux 4.9+ 에 기본 탑재. sysctl net.ipv4.tcp_congestion_control=bbr 로 활성화.

3.9 BBR 의 논란

BBR v1 이 Cubic 과 공존할 때 BBR 이 대역폭을 더 가져감 → "공정성 문제". BBRv2 (2019), v3 (2023) 에서 개선.

알고리즘특징용도
RenoAIMD, 고전레거시
Cubic3차 함수 증가Linux 기본, 장거리
BBRBW/RTT 직접 측정고속 + 비혼잡 손실
DCTCP데이터센터 내부, ECN 마킹DC 전용
CoPA저지연 우선화상 회의

4. Nagle 과 Delayed ACK — 최악의 만남

4.1 Nagle 알고리즘

작은 패킷이 많으면 오버헤드 (40바이트 헤더 / 1바이트 데이터 → 효율 2.5%).

Nagle: "전송 중인 작은 패킷이 있으면, 새로운 작은 데이터는 ACK 올 때까지 기다린 후 합쳐서 보내라."

setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int));  // Nagle 끄기

효율에 좋지만 지연 추가.

4.2 Delayed ACK

수신자도 효율을 위해 ACK 을 즉시 안 보내고 모아 보냄:

  • 다음 데이터 전송 시 같이 (piggyback).
  • 또는 최대 200ms 대기 (Linux 기본은 40ms).

4.3 Nagle + Delayed ACK = 40ms 지연

A: 첫 작은 패킷 보냄
B: ACK 보류 (더 보내겠지?)
A: 두 번째 작은 패킷 있음, 하지만 NagleACK 기다림
Nagle: "ACK 안 오네? 기다리자"
Delayed ACK: "데이터 안 오네? 200ms 후 ACK 보내자"
   → 40ms 후 ACK 도착
A: 두 번째 패킷 전송

실시간 앱 (Telnet, SSH, 원격 게임) 에서 명시적으로 TCP_NODELAY 설정 필수.

4.4 TCP_CORK — 반대 방향 튜닝

"버퍼에 모아서 한 번에 보내라". sendfile + TCP_CORK 조합이 nginx 의 정적 파일 최적화.

int cork = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(int));
writev(fd, iov, 10);
cork = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(int));  // flush

5. TCP Fast Open — 3-Way Handshake 건너뛰기

HTTP 요청마다 SYN → SYN+ACK → ACK → 요청 → 응답 = 최소 1.5 RTT 지연.

5.1 TFO (RFC 7413, 2014)

첫 연결 시 서버가 쿠키 를 보냄. 이후 연결에서 클라이언트가 SYN 에 쿠키 + 요청 데이터를 실어서 보냄.

1st connection:  SYNSYN+ACK(cookie)ACKGET
2nd connection:  SYN(cookie, GET)SYN+ACK(data)

한 RTT 절약. CDN, 모바일 앱에서 유효.

5.2 왜 잘 안 쓰이는가

  • 미들박스 (방화벽, NAT) 가 비표준 SYN 을 버림.
  • 서버 쿠키 상태 관리 부담.
  • HTTP/2 의 connection reuse 가 어느 정도 상쇄.

QUIC 의 0-RTT handshake 가 이 문제를 우회.

6. 흔한 에러들의 진짜 의미

6.1 Connection refused

  • 의미: SYN 에 RST 응답. 상대 포트에 listen 안 함.
  • 원인: 서버 미실행, 방화벽, 잘못된 포트.

6.2 Connection reset by peer

  • 의미: 양측이 ESTABLISHED 였는데 RST 받음.
  • 원인: 상대 프로세스 크래시, SO_LINGER 0 으로 강제 종료, 방화벽의 active reset.

6.3 Broken pipe

  • 의미: 상대가 이미 닫은 소켓에 write 시도.
  • 원인: 한쪽이 close 한 줄 모르고 계속 씀. (TCP 는 양방향이라 한쪽이 닫아도 읽기는 가능할 수 있음.)

6.4 Connection timed out

  • 의미: SYN 을 여러 번 보냈는데 응답 없음 (보통 5~7회 재전송 후, 60초+).
  • 원인: 네트워크 불통, 블랙홀 방화벽, 상대 서버 다운.

6.5 No route to host

  • 의미: 라우팅 테이블에 대상 IP 경로 없음.
  • 원인: VPN 끊김, 라우팅 설정 오류.

7. 실무 튜닝 — 주요 sysctl

7.1 백로그와 큐

net.core.somaxconn          # listen 의 backlog 상한 (기본 4096, 권장 65535)
net.ipv4.tcp_max_syn_backlog # SYN_RCVD 큐 크기
net.core.netdev_max_backlog # NIC → 커널 큐 크기

nginx listen 80 backlog=65535 는 앱 쪽 백로그. 커널 상한도 같이 올려야.

7.2 TIME_WAIT 관련

net.ipv4.tcp_tw_reuse=1        # ephemeral port 재사용 (클라이언트 쪽에 유용)
net.ipv4.tcp_fin_timeout=30    # FIN_WAIT_2 대기 시간
net.ipv4.ip_local_port_range   # 1024 65535 로 확대

7.3 Keep-alive

net.ipv4.tcp_keepalive_time=600    # idle 감지까지 (기본 2시간!)
net.ipv4.tcp_keepalive_intvl=60    # probe 간격
net.ipv4.tcp_keepalive_probes=3    # 최대 probe

기본 2시간은 너무 길다. LB 뒤 서버는 보통 10분 정도.

7.4 버퍼 크기

net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem="4096 131072 16777216"   # min default max
net.ipv4.tcp_wmem="4096 131072 16777216"

BDP 만큼의 버퍼 필요. 10Gbps × 50ms = 62MB.

7.5 혼잡 제어

net.ipv4.tcp_congestion_control=bbr
net.core.default_qdisc=fq   # BBR 권장 qdisc

8. QUIC — TCP 를 버리다

8.1 TCP 의 근본적 한계

TCP 가 수십 년 진화했지만 바꿀 수 없는 한계:

  1. HoL (Head-of-Line) Blocking: 한 패킷 손실 시 뒤 패킷들 모두 대기. HTTP/2 도 같은 TCP 연결 위에서 이 문제.
  2. Handshake 비용: TCP 3-way + TLS 1.2 2-RTT = 3 RTT 지연.
  3. 미들박스가 굳어짐: 새 TCP 옵션을 도입해도 ISP 방화벽이 차단. TFO, MPTCP, TCP Fast Open 이 실패한 이유.
  4. 커널 의존성: TCP 구현 개선은 커널 업그레이드 필요. 느림.

8.2 QUIC 의 해법 — UDP 위에 유저 공간 전송 프로토콜

2012년 구글 내부 실험 시작 → IETF 표준 (RFC 9000, 2021) → HTTP/3 기반.

  • UDP 위에 구현: 미들박스는 일반 UDP 로만 봄.
  • 유저 공간 라이브러리: 커널 업그레이드 없이 앱과 함께 배포.
  • TLS 1.3 필수 통합: 암호화 + 전송을 하나로.
  • 스트림 다중화 (진짜): 각 스트림 독립 순서 → HoL 없음.

8.3 0-RTT Handshake

재접속 시 이전 session ticket 으로 첫 패킷에 데이터 실어서 전송. TLS 1.3 의 0-RTT 와 결합.

처음:     QUIC Handshake (1 RTT)
재접속:   데이터부터 바로 (0 RTT)

8.4 Connection Migration

IP 가 바뀌어도 연결 유지. 모바일에서 WiFi ↔ 4G 전환 시 끊김 없음. Connection ID 로 인식 (소스 IP+포트 의존하지 않음).

8.5 HTTP/3 = HTTP over QUIC

  • HTTP 의미는 HTTP/2 와 동일.
  • 전송만 TCP+TLS → QUIC.
  • 주요 브라우저 + CDN (Cloudflare, Akamai, Fastly) 모두 지원.

8.6 QUIC 의 단점

  • CPU 사용량 증가: 암호화가 강제이고 유저 공간에서 실행.
  • 미들박스 호환성: 일부 방화벽이 UDP 스루풋 제한.
  • 구현 복잡도: TCP 보다 훨씬 크고 움직이는 부품 많음.
  • 관찰 어려움: 암호화된 페이로드, 디버깅 도구 적음.

구글, 메타 내부 측정: HTTP/3 가 HTTP/2 보다 모바일에서 10% 지연 감소. 데스크톱에서는 차이 작음.

9. 실전 디버깅 툴킷

9.1 연결 상태 보기

ss -tan        # 모든 TCP 연결
ss -tan state established     # ESTABLISHED 만
ss -tnp | grep :443           # HTTPS 연결 + PID
ss -s                          # 요약 통계

9.2 패킷 캡처

tcpdump -i any -w capture.pcap 'port 443'
wireshark capture.pcap   # GUI 로 상세 분석

팁: tcpdump -A 로 ASCII 출력, HTTP 헤더 빠르게 확인.

9.3 혼잡 제어 관찰

ss -ti    # 각 연결의 CC 알고리즘, cwnd, rtt, retrans 카운트

출력 예:

ESTAB  ... cubic cwnd:10 ssthresh:7 bytes_acked:1234 bytes_received:5678 rtt:25.3/3.1 rcv_rtt:25.1 delivered:10 ...

cwnd 가 작고 retrans 가 많으면 혼잡 발생 중.

9.4 bpftrace 로 내부 들여다보기

bpftrace -e 'kprobe:tcp_retransmit_skb { printf("retrans pid=%d\n", pid); }'

커널 내 재전송, 상태 전이, 혼잡 이벤트를 실시간으로 관찰.

9.5 Mtr — traceroute + ping

mtr -r -c 100 example.com

경로의 각 홉별 손실률 + RTT. ISP 품질 문제 진단.

10. 마치며 — 50년 된 TCP, 그리고 그 너머

1974년 Vint Cerf 와 Bob Kahn 의 원 논문에서 시작된 TCP 는 인터넷의 근간이 됐다. 50년간:

  • 1988 Jacobson: 혼잡 제어.
  • 1992 Window Scaling.
  • 1996 SACK.
  • 2006 Cubic.
  • 2016 BBR.
  • 2021 QUIC (RFC 표준).

그러나 QUIC 로 전송 계층이 유저 공간으로 이동하면서 새 시대가 열렸다. 각 앱이 자기만의 전송 전략을 가질 수 있고, 커널 업그레이드 없이 진화할 수 있다. Facebook 의 mvfst, Cloudflare 의 quiche, 구글의 gQUIC — 이들이 앞으로 인터넷의 모양을 정할 것이다.

동시에 TCP 는 사라지지 않는다. 수십 년 된 시스템, 레거시 프로토콜, 커널 필수 기능으로 여전히 필수. "TCP 는 죽었다" 는 말은 매년 나오지만 트래픽의 80%+ 가 여전히 TCP 다.

다음 글에서는 TLS/SSL 과 PKI 의 내부 — 인증서 체인, 암호 스위트, 0-RTT 의 replay 위험, QUIC 의 TLS 통합 방식, 그리고 Post-Quantum 암호의 도래 — 를 파볼 예정이다.

참고 자료

  • RFC 9293 — Transmission Control Protocol (2022 개정판).
  • Van Jacobson — "Congestion Avoidance and Control" (SIGCOMM, 1988).
  • Cardwell et al — "BBR: Congestion-Based Congestion Control" (ACM Queue, 2016).
  • RFC 9000 — QUIC: A UDP-Based Multiplexed and Secure Transport.
  • RFC 9114 — HTTP/3.
  • "Computer Networks: A Systems Approach" — Peterson & Davie.
  • Brendan Gregg — Linux Performance 블로그의 TCP 관련 글.
  • Marek Majkowski (Cloudflare) — "The story of one latency spike" 등 TCP 내부 글.
  • "High Performance Browser Networking" — Ilya Grigorik (O'Reilly, 2013).

TCP Network Stack Deep Dive — State Machine, Congestion Control (Cubic vs BBR), Nagle, Delayed ACK, and the Evolution to QUIC (2025)

0. The TCP You Think You Know

Every HTTP request rides on TCP. Yet consider:

  • Why does TIME_WAIT in ss -tan sit around for 30 seconds?
  • What distinguishes "connection reset by peer" from "broken pipe"?
  • Why can sending 100 messages of 1KB per second take 4 seconds?
  • Why does iperf deliver 1Gbps on a 10Gbps link?
  • Why did Google build QUIC on UDP instead of TCP?

The answers live below: TCP state machine, congestion control evolution, infamous interaction bugs, and the QUIC era.

1. TCP State Machine — 11 States

1.1 Connection Setup: 3-Way Handshake

Client              Server
  |  SYN (seq=x)      |
  |----------------->|  [LISTEN -> SYN_RCVD]
  | SYN+ACK(y,x+1)   |
  |<-----------------|
  |  ACK (ack=y+1)   |
  |----------------->|  [SYN_RCVD -> ESTABLISHED]

Why three, not two? Bidirectional ISN (Initial Sequence Number) synchronization. Each direction must announce its own ISN and receive acknowledgment.

1.2 Connection Teardown: 4-Way Handshake

A                B
 |   FIN        |
 |------------->|  [ESTABLISHED -> CLOSE_WAIT]
 |   ACK        |
 |<-------------|
 [FIN_WAIT_1 -> FIN_WAIT_2]
 |   FIN        |
 |<-------------|  [CLOSE_WAIT -> LAST_ACK]
 |   ACK        |
 |------------->|
 [FIN_WAIT_2 -> TIME_WAIT]  [LAST_ACK -> CLOSED]

Four, because TCP is full-duplex. Each direction closes independently.

1.3 TIME_WAIT — Misunderstood

Wait 2MSL (Maximum Segment Lifetime) — typically 30s to 2 minutes. Reasons:

  1. Prevent stray packets from joining a new connection: quickly reopened port pairs could receive delayed packets from the previous connection.
  2. Handle lost final ACK: if the peer retransmits FIN, we must respond with ACK; a CLOSED state would reply with RST.

1.4 TIME_WAIT Explosion

Short-lived outbound connections pile up tens of thousands of TIME_WAIT, exhausting ephemeral ports.

Wrong fix: forcing TIME_WAIT to 0 (unsafe, risks data corruption).

Right fixes:

  • net.ipv4.tcp_tw_reuse=1: reuse ephemeral port safely.
  • Keep-Alive connections (HTTP/1.1 persistent, HTTP/2 multiplexing).
  • Connection pools in DB drivers and HTTP clients.

1.5 CLOSE_WAIT — App Bug Signal

TIME_WAIT is normal; stacked CLOSE_WAIT is not. The peer sent FIN but your app never called close().

$ ss -tan | grep CLOSE_WAIT | wc -l
50000   # socket leak

Cause: missing try-finally on file descriptors.

2. TCP Reliability Mechanisms

2.1 Sequence Numbers and ACK

Every byte has a sequence number. The receiver ACKs the next expected byte.

Send:  [1000][1001][1002][1003]
Recv:  ACK=1004  (got 1000-1003, expect 1004)

If a segment is lost, the receiver repeats the same ACK (duplicate ACK). Three dup ACKs trigger Fast Retransmit.

2.2 Retransmission — RTO and Fast Retransmit

  • RTO: dynamic timeout based on RTT measurement (Jacobson, 1988).
  • Fast Retransmit: skip timeout on 3 dup ACKs; recovery within milliseconds.
  • SACK: precise "got 1000-2000, 3000-4000, missing 2000-3000".

2.3 Flow Control — Receive Window

Receivers advertise rwnd. Senders only send while unacked_bytes < rwnd.

2.4 Window Scaling

TCP's 16-bit window = 65,535 bytes. 10Gbps x 100ms RTT = 125MB needed. RFC 1323 scaling multiplies rwnd by 2^shift.

sysctl net.ipv4.tcp_window_scaling
sysctl net.core.rmem_max
sysctl net.ipv4.tcp_rmem

3. Congestion Control

3.1 The 1986 Congestion Collapse

UC Berkeley to LBL link dropped from 32Kbps to 40bps — 1000x degradation. Cause: retransmit storms. Van Jacobson's 1988 paper introduced congestion control.

3.2 Congestion Window (cwnd)

send = min(rwnd, cwnd)

rwnd comes from the receiver; cwnd is the sender's estimate of network capacity.

3.3 Slow Start

Initial cwnd = 10 MSS. Each ACK increments cwnd by 1 MSS, doubling per RTT.

cwnd: 10 -> 20 -> 40 -> 80 -> 160

Exponential growth in practice despite the "slow" name.

3.4 Congestion Avoidance

After ssthresh, cwnd += 1 MSS per RTT (linear).

3.5 Loss Detection

  • 3 Dup ACK (Fast Retransmit): halve cwnd.
  • Timeout: cwnd = 1 MSS, restart Slow Start.

This is AIMD (Additive Increase, Multiplicative Decrease) — the Reno core.

3.6 Reno to Cubic

Reno is too conservative on long fat networks. CUBIC (Linux default) grows cwnd as a cubic function of time, remembers the last loss point, and probes beyond it.

3.7 BBR — Google's 2016 Revolution

Traditional algorithms assume loss = congestion. Modern networks see non-congestive loss (WiFi noise, bufferbloat).

BBR (Bottleneck Bandwidth and RTT) idea:

"Measure actual bandwidth and RTT directly; size cwnd from them."

  • Periodically probe for bandwidth.
  • Track minimum RTT (detect bufferbloat).
  • Keep cwnd near BW x RTT — minimize queuing.

3.8 BBR Results

Google google.com and YouTube after BBR:

  • 4% reduction in YouTube rebuffering.
  • Lower google.com response time.
  • Large gains in developing regions.

Linux 4.9+ includes BBR. Enable with sysctl net.ipv4.tcp_congestion_control=bbr.

3.9 BBR Fairness Debate

BBR v1 took more bandwidth than Cubic when coexisting — a fairness issue. Improved in BBRv2 (2019) and v3 (2023).

AlgorithmTraitUse
RenoAIMD, classicLegacy
CubicCubic growthLinux default, WAN
BBRDirect BW/RTTHigh speed, non-congestive loss
DCTCPECN markingData center
CoPALow latencyVideo conferencing

4. Nagle and Delayed ACK — The Worst Pairing

4.1 Nagle

Small packets are expensive (40-byte header for 1-byte payload = 2.5% efficiency).

Nagle: "If an unacked small packet is in flight, buffer further small data until ACK arrives."

setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(int));

Good for throughput; adds latency.

4.2 Delayed ACK

The receiver also batches ACKs — piggyback on next data, or wait up to 200ms (40ms default on Linux).

4.3 Nagle + Delayed ACK = 40ms Stall

A: sends first small packet
B: defers ACK (more might come)
A: wants to send second, Nagle waits for ACK
   -> Nagle waits for ACK
   -> Delayed ACK waits for data
   -> 40ms later, ACK fires
A: finally sends second

Real-time apps (Telnet, SSH, remote games) must set TCP_NODELAY.

4.4 TCP_CORK — The Opposite

"Buffer up, send in one shot." sendfile + TCP_CORK powers nginx static-file delivery.

int cork = 1;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(int));
writev(fd, iov, 10);
cork = 0;
setsockopt(fd, IPPROTO_TCP, TCP_CORK, &cork, sizeof(int));

5. TCP Fast Open — Skip the Handshake

Each HTTP request costs at least 1.5 RTT for handshake plus request.

5.1 TFO (RFC 7413, 2014)

First connection: server issues a cookie. Later connections send SYN with cookie + request payload.

1st:  SYN -> SYN+ACK(cookie) -> ACK -> GET
2nd:  SYN(cookie, GET) -> SYN+ACK(data)

5.2 Why It Flopped

  • Middleboxes drop non-standard SYN.
  • Server-side cookie state.
  • HTTP/2 reuse mitigates the pain.

QUIC's 0-RTT handshake sidesteps this.

6. Common Errors Decoded

6.1 Connection refused

RST to SYN — no listener on that port.

6.2 Connection reset by peer

RST during ESTABLISHED — peer crashed, SO_LINGER 0, or firewall active reset.

6.3 Broken pipe

Write to a socket the peer already closed.

6.4 Connection timed out

SYNs gone unanswered (5-7 retries, 60s+). Network black hole or dead server.

6.5 No route to host

Routing table has no path — VPN dropped, misconfigured route.

7. Production Tuning — Key sysctls

7.1 Backlog and Queues

net.core.somaxconn            # listen backlog cap
net.ipv4.tcp_max_syn_backlog  # SYN_RCVD queue
net.core.netdev_max_backlog   # NIC to kernel queue

nginx listen 80 backlog=65535 needs matching kernel caps.

7.2 TIME_WAIT

net.ipv4.tcp_tw_reuse=1
net.ipv4.tcp_fin_timeout=30
net.ipv4.ip_local_port_range

7.3 Keep-alive

net.ipv4.tcp_keepalive_time=600
net.ipv4.tcp_keepalive_intvl=60
net.ipv4.tcp_keepalive_probes=3

Defaults of 2 hours are too long for load-balanced services.

7.4 Buffers

net.core.rmem_max=16777216
net.core.wmem_max=16777216
net.ipv4.tcp_rmem="4096 131072 16777216"
net.ipv4.tcp_wmem="4096 131072 16777216"

Match BDP: 10Gbps x 50ms = 62MB.

7.5 Congestion Control

net.ipv4.tcp_congestion_control=bbr
net.core.default_qdisc=fq

8. QUIC — Leaving TCP Behind

8.1 TCP's Hard Limits

  1. Head-of-Line Blocking: one lost packet stalls all streams on the same TCP connection — HTTP/2 inherits this.
  2. Handshake cost: TCP 3-way + TLS 1.2 2-RTT = 3 RTT.
  3. Middlebox ossification: new TCP options get dropped by ISP firewalls. TFO and MPTCP failed here.
  4. Kernel coupling: TCP improvements require kernel upgrades.

8.2 QUIC's Answer — User-Space Transport over UDP

Google experiment (2012), IETF standard (RFC 9000, 2021), HTTP/3 base.

  • Built on UDP — middleboxes just see UDP.
  • User-space library — ships with the app.
  • TLS 1.3 integrated.
  • True stream multiplexing — no HoL across streams.

8.3 0-RTT Handshake

Reconnects piggyback data on the first packet using a session ticket.

First:     QUIC handshake (1 RTT)
Reconnect: data immediately (0 RTT)

8.4 Connection Migration

Connection ID survives IP changes. WiFi to 4G handover without dropping.

8.5 HTTP/3 = HTTP over QUIC

Same semantics as HTTP/2, transport swap to QUIC. Major browsers and CDNs (Cloudflare, Akamai, Fastly) support it.

8.6 QUIC Downsides

  • Higher CPU (mandatory crypto, user space).
  • Middlebox incompatibility with heavy UDP.
  • Implementation complexity.
  • Harder to observe — encrypted payloads, fewer tools.

Google and Meta measured ~10% latency improvement on mobile; desktop gains are smaller.

9. Debugging Toolkit

9.1 Inspect Connections

ss -tan
ss -tan state established
ss -tnp | grep :443
ss -s

9.2 Packet Capture

tcpdump -i any -w capture.pcap 'port 443'
wireshark capture.pcap

9.3 Congestion Control Observation

ss -ti

Sample:

ESTAB  ... cubic cwnd:10 ssthresh:7 bytes_acked:1234 bytes_received:5678 rtt:25.3/3.1 rcv_rtt:25.1 delivered:10 ...

Small cwnd with many retrans = congestion.

9.4 bpftrace

bpftrace -e 'kprobe:tcp_retransmit_skb { printf("retrans pid=%d\n", pid); }'

9.5 Mtr

mtr -r -c 100 example.com

Per-hop loss + RTT for ISP diagnosis.

10. Closing — 50 Years of TCP, and What Comes Next

TCP dates to Vint Cerf and Bob Kahn's 1974 paper. Highlights:

  • 1988: Jacobson congestion control.
  • 1992: Window Scaling.
  • 1996: SACK.
  • 2006: Cubic.
  • 2016: BBR.
  • 2021: QUIC (RFC).

QUIC pulled transport into user space. Apps can evolve their transport without kernel upgrades — Facebook mvfst, Cloudflare quiche, Google gQUIC will shape the next decade. Meanwhile TCP stays: 80%+ of traffic still runs on it.

Next post: TLS/SSL and PKI internals — cert chains, cipher suites, 0-RTT replay risk, QUIC crypto integration, and post-quantum.

References

  • RFC 9293 — Transmission Control Protocol (2022 revision).
  • Van Jacobson — "Congestion Avoidance and Control" (SIGCOMM, 1988).
  • Cardwell et al — "BBR: Congestion-Based Congestion Control" (ACM Queue, 2016).
  • RFC 9000 — QUIC.
  • RFC 9114 — HTTP/3.
  • "Computer Networks: A Systems Approach" — Peterson & Davie.
  • Brendan Gregg — Linux Performance blog.
  • Marek Majkowski (Cloudflare) — TCP internals writing.
  • "High Performance Browser Networking" — Ilya Grigorik (O'Reilly, 2013).