
TCP vs UDP: 꼼꼼한 집사와 쿨한 배달부 (완전정복)
TCP의 흐름 제어, 혼잡 제어, 3-Way Handshake부터 UDP의 홀 펀칭, 헤더 구조 비교, 그리고 게임 개발자를 위한 Nagle 알고리즘과 TCP_NODELAY 옵션까지.

TCP의 흐름 제어, 혼잡 제어, 3-Way Handshake부터 UDP의 홀 펀칭, 헤더 구조 비교, 그리고 게임 개발자를 위한 Nagle 알고리즘과 TCP_NODELAY 옵션까지.
내 서버는 왜 걸핏하면 뻗을까? OS가 한정된 메모리를 쪼개 쓰는 처절한 사투. 단편화(Fragmentation)와의 전쟁.

미로를 탈출하는 두 가지 방법. 넓게 퍼져나갈 것인가(BFS), 한 우물만 팔 것인가(DFS). 최단 경로는 누가 찾을까?

이름부터 빠릅니다. 피벗(Pivot)을 기준으로 나누고 또 나누는 분할 정복 알고리즘. 왜 최악엔 느린데도 가장 많이 쓰일까요?

매번 3-Way Handshake 하느라 지쳤나요? 한 번 맺은 인연(TCP 연결)을 소중히 유지하는 법. HTTP 최적화의 기본.

제 첫 프로젝트는 간단한 실시간 채팅 앱이었습니다. 파이썬으로 소켓 프로그래밍을 배우면서 서버를 만들었고, 테스트해보니 메시지가 잘 전달되더군요. "오, 됐다!" 하고 친구들에게 자랑했습니다.
그런데 친구 하나가 물었습니다. "근데 너 TCP 써? UDP 써?"
저는 멍했습니다.
SOCK_STREAM이라고 썼으니 TCP인 건 알았는데, "왜 TCP인지", "UDP였으면 뭐가 달랐을지" 전혀 몰랐습니다.
그냥 튜토리얼이 그렇게 하라니까 따라한 거였죠.
그날 밤, TCP와 UDP의 차이를 이해하려고 수십 개 블로그를 뒤졌습니다. "TCP는 신뢰성, UDP는 속도"라는 말은 외웠는데, 실제로 어떤 메커니즘으로 신뢰성을 보장하는지, 왜 UDP가 빠른지 이해가 안 갔습니다. 특히 "3-Way Handshake", "Sliding Window", "Nagle 알고리즘" 같은 용어들은 도대체 뭔지 감이 안 잡혔습니다.
그러다 한 글에서 "TCP는 등기우편, UDP는 일반 엽서"라는 비유를 봤습니다. 등기우편은 받는 사람이 서명해야 하고, 안 받으면 재발송됩니다. 엽서는 그냥 우체통에 넣고 끝입니다. 이 비유를 듣자마자 머릿속에 딱 그림이 그려졌습니다.
결국 제가 이해한 건 이겁니다. "TCP는 '택배 기사가 문 앞에서 기다리는 시스템', UDP는 '우체통에 던지고 가는 시스템'"이었습니다. 이 글은 그 이해의 과정을 정리한 노트입니다.
처음에 저는 TCP가 "신뢰성"만 높은 줄 알았습니다. 그런데 막상 성능 테스트를 해보니, TCP가 UDP보다 2~3배 느렸습니다.
왜 그럴까요? TCP는 데이터를 보내기 전에 3-Way Handshake라는 걸 합니다. 이게 뭔지 몰랐을 땐 "그냥 인사 정도겠지" 했는데, 실제로는 네트워크 왕복(Round Trip)을 3번 해야 하는 작업이었습니다.
TCP는 데이터를 보내기 전에 반드시 "연결"을 맺어야 합니다. 이 연결을 맺는 과정이 바로 3-Way Handshake입니다.
이 3단계가 끝나야 비로소 데이터를 보낼 수 있습니다. 인터넷이 빠르면 몇 밀리초지만, 해외 서버라면 수백 밀리초가 걸립니다.
저는 처음에 "이거 왜 필요해? 그냥 보내면 되잖아?"라고 생각했습니다. 그런데 알고 보니, 이 과정이 "상대방이 살아있고, 받을 준비가 됐는지"를 확인하는 유일한 방법이었습니다.
이 3-Way Handshake는 보안 취약점도 만듭니다. 해커가 SYN 패킷만 수백만 개 보내고 도망가는 공격이 바로 SYN Flood입니다.
서버는 "SYN-ACK"를 보내고 클라이언트의 "ACK"를 기다립니다. 그런데 ACK가 오지 않으면, 서버는 메모리에 이 연결 정보를 계속 들고 있습니다. 공격자가 이걸 악용하면 서버 메모리가 터집니다.
방어책은 SYN Cookie입니다. 서버가 메모리에 저장하지 않고, 암호화된 쿠키로 연결 정보를 만들어 돌려줍니다. 이걸 이해했을 때, "아, TCP는 신뢰성을 위해 이런 오버헤드를 감수하는구나"라고 받아들였습니다.
TCP와 UDP의 차이를 한 줄로 이해한 순간은 이 비유를 만났을 때입니다.
"TCP는 집사(Butler), UDP는 배달부(Courier)"집사는 주인이 "물 가져와"라고 하면, 물을 가져다주고 "받으셨나요?"라고 확인합니다. 주인이 물을 못 받았으면 다시 가져옵니다. 배달부는 그냥 문 앞에 던지고 갑니다. 받았는지 안 받았는지 확인 안 합니다.
이 비유가 와닿았던 이유는, 제가 만든 채팅 앱이 "메시지 하나라도 빠지면 안 되는 서비스"였기 때문입니다. "안녕"이라는 메시지가 안 가면, 상대방은 제가 무시했다고 오해할 수 있습니다. 그래서 TCP가 필요했던 겁니다.
반면, 만약 제가 실시간 게임을 만들었다면 UDP를 썼을 겁니다. 게임에서 "캐릭터 위치 (x=100, y=200)" 패킷이 유실돼도, 0.1초 뒤에 "x=105, y=205"가 오면 됩니다. 오래된 패킷을 기다릴 필요가 없습니다.
TCP가 신뢰성을 보장하는 방법은 크게 4가지입니다.
TCP는 모든 패킷에 시퀀스 번호(Sequence Number)를 붙입니다. 예를 들어, "안녕하세요"를 보낸다면:
만약 패킷이 "3 → 1 → 2" 순서로 도착하면, 받는 쪽에서 "1 → 2 → 3"으로 재조립합니다. 이 덕분에 우리가 웹사이트를 볼 때 글자가 뒤섞이지 않는 겁니다.
UDP는 이 기능이 없습니다. 그냥 도착한 순서대로 처리합니다.
TCP는 패킷을 보낸 후 ACK(Acknowledgment)를 기다립니다. ACK가 안 오면 "아, 패킷이 중간에 사라졌구나" 하고 재전송합니다.
이 메커니즘을 이해하려고 제가 직접 실험을 했습니다.
네트워크 지연을 시뮬레이션하는 도구(tc 명령어)로 패킷 유실률을 30%로 설정했습니다.
그러자 TCP는 자동으로 재전송을 하면서 데이터를 완벽하게 전달했습니다.
UDP는 30%의 데이터를 그냥 잃어버렸습니다.
재전송에는 타임아웃(Timeout) 메커니즘이 중요합니다. TCP는 "얼마나 기다릴 것인가?"를 RTT(Round Trip Time)를 기반으로 동적으로 계산합니다. 네트워크가 빠르면 짧게 기다리고, 느리면 길게 기다립니다.
이걸 이해했을 때, "TCP는 단순히 재전송만 하는 게 아니라, 네트워크 상황을 실시간으로 학습하는구나"라고 받아들였습니다.
받는 쪽(Receiver)이 처리할 수 있는 속도보다 빠르게 데이터가 오면 버퍼가 넘칩니다. TCP는 이걸 방지하기 위해 Sliding Window 기법을 씁니다.
받는 쪽이 "나는 지금 1024바이트밖에 못 받아(Window Size = 1024)"라고 알려주면, 보내는 쪽은 그만큼만 보냅니다. 버퍼가 비면 Window Size가 커지고, 다시 꽉 차면 작아집니다.
저는 이걸 처음 배울 때 "왜 이렇게 복잡하게 만들었을까?" 싶었습니다. 그런데 실제로 제 채팅 서버를 수십 명이 동시에 접속하게 했을 때, 서버가 처리 못 해서 프로그램이 죽더군요. 흐름 제어가 없으면 이런 일이 생긴다는 걸 몸소 체험했습니다.
흐름 제어는 "받는 쪽"의 문제를 해결하지만, 혼잡 제어는 "네트워크 전체"의 문제를 해결합니다.
인터넷은 공유 자원입니다. 만약 모든 사람이 동시에 최대 속도로 데이터를 보내면, 라우터가 터집니다. TCP는 이걸 방지하기 위해 Slow Start라는 알고리즘을 씁니다.
처음엔 패킷 1개만 보냅니다. ACK가 잘 오면 2개, 4개, 8개... 이렇게 지수적으로 늘립니다. 그러다 패킷 유실이 발생하면 "아, 네트워크가 막히나 보다" 하고 속도를 확 줄입니다.
이 메커니즘은 공평성(Fairness)을 보장합니다. 모든 TCP 연결이 네트워크 대역폭을 공평하게 나눠 씁니다. UDP는 이 기능이 없어서, UDP 트래픽이 많으면 TCP 연결이 굶어 죽을 수 있습니다.
UDP는 TCP의 모든 기능을 제거했습니다.
그냥 "패킷을 만들어서 던진다"가 끝입니다.
UDP는 "신뢰성이 필요하면 애플리케이션 레벨에서 직접 구현해라"는 철학입니다. 이게 처음엔 이해가 안 갔습니다. "그럼 다들 TCP 쓰면 되잖아?"
그런데 알고 보니, UDP가 필요한 이유가 명확했습니다.
제가 UDP를 진짜 이해한 건, P2P 화상 통화를 구현하려다가 NAT 문제를 만났을 때입니다.
집에 있는 공유기(NAT) 뒤의 컴퓨터는 "외부에서 직접 접속할 수 없습니다". 공유기가 외부 IP 하나를 여러 기기가 공유하기 때문입니다.
TCP는 이 문제를 해결하기 어렵습니다. "클라이언트 → 서버"는 되지만, "클라이언트 ↔ 클라이언트"는 안 됩니다.
UDP는 Hole Punching 기법으로 이 문제를 해결합니다.
WebRTC(화상 통화), 온라인 게임이 모두 이 기법을 씁니다. 저는 이걸 이해하고 나서 "UDP가 단순한 게 아니라, 자유도가 높은 거구나"라고 생각이 바뀌었습니다.
제가 실시간 채팅 앱을 만들면서 겪은 가장 이상한 버그는 "가끔 메시지가 0.2초 늦게 가는 현상"이었습니다.
디버깅을 해보니, 메시지를 보내는 코드는 즉시 실행되는데, 실제로 패킷이 전송되는 건 0.2초 뒤였습니다. 알고 보니 Nagle 알고리즘 때문이었습니다.
1980년대 인터넷은 느렸습니다. 작은 패킷 수천 개를 보내면 네트워크가 막혔습니다. Nagle 알고리즘은 이걸 해결하려고 만들어졌습니다.
"작은 데이터는 모았다가 한 번에 보낸다"예를 들어, "H", "e", "l", "l", "o"를 1바이트씩 5번 보내는 대신, 0.2초 기다렸다가 "Hello" 5바이트를 한 번에 보냅니다. 네트워크 효율은 올라가지만, 지연(Latency)이 생깁니다.
게임이나 실시간 채팅은 "지연 0.2초"가 치명적입니다. 그래서 개발자들은 TCP_NODELAY 옵션을 켭니다.
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Nagle 끄기
이 옵션을 켜면, 1바이트짜리 패킷도 즉시 보냅니다. 네트워크 부하는 약간 늘어나지만, 반응 속도는 확 좋아집니다.
저는 이 옵션을 켜고 나서 채팅 앱의 "답답함"이 사라지는 걸 체감했습니다. "아, TCP를 쓰려면 이런 것까지 알아야 하는구나"라고 받아들였습니다.
TCP와 UDP의 차이는 헤더를 보면 명확합니다.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
핵심 필드 설명:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
그게 끝입니다. 포트 번호, 길이, 체크섬만 있습니다.
이 헤더를 보고 나서 "UDP가 왜 빠른지" 이해했습니다. TCP는 헤더 처리만으로도 CPU 연산이 많습니다. UDP는 헤더를 거의 안 봅니다.
이론만 보면 와닿지 않아서, 직접 코드를 짜봤습니다.
import socket
# TCP 서버 생성
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Nagle 끄기
server.bind(('0.0.0.0', 9000))
server.listen(5)
print("TCP 서버 대기 중...")
while True:
conn, addr = server.accept() # 3-Way Handshake 발생
print(f"연결됨: {addr}")
while True:
data = conn.recv(1024)
if not data:
break
print(f"받음: {data.decode()}")
conn.send(data) # Echo
conn.close()
특징:
accept()에서 3-Way Handshake가 일어납니다.recv()는 순서가 보장된 데이터를 받습니다.data가 비어서 break됩니다.import socket
# UDP 서버 생성
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(('0.0.0.0', 9000))
print("UDP 서버 대기 중...")
while True:
data, addr = server.recvfrom(1024) # 연결 없이 바로 받음
print(f"로그 from {addr}: {data.decode()}")
# 응답 안 해도 됨 (Fire and Forget)
특징:
accept() 없음. 연결 개념이 없습니다.recvfrom()으로 패킷을 받으면, 보낸 사람 주소가 같이 옵니다.TCP 클라이언트:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 9000)) # 3-Way Handshake
client.send(b"Hello TCP")
response = client.recv(1024)
print(response)
client.close()
UDP 클라이언트:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b"Hello UDP", ('localhost', 9000)) # 그냥 던짐
# 응답 받고 싶으면: data, addr = client.recvfrom(1024)
이 코드를 짜고 나서 "TCP는 전화 통화(연결 후 대화), UDP는 무전기(그냥 송신)"라는 비유가 확 와닿았습니다.
제가 프로젝트마다 TCP/UDP를 선택한 기준은 이겁니다.
제가 이 기준을 정리하고 나서, "무조건 TCP가 좋은 게 아니구나, 상황에 따라 다르구나"라고 받아들였습니다.
최근 구글이 만든 QUIC 프로토콜이 웹 표준(HTTP/3)이 되었습니다. 이게 제게 충격이었던 이유는, "TCP를 버렸다"는 점입니다.
TCP는 패킷 1번이 유실되면, 패킷 2, 3, 4가 이미 도착했어도 기다립니다. 순서를 맞춰야 하니까요.
웹페이지를 로딩할 때, 이미지 10개를 동시에 받는다고 합시다. 이미지 1번 패킷이 유실되면, 이미지 2~10번은 이미 다 받았는데도 화면에 안 뜹니다. TCP가 1번을 재전송받을 때까지 블로킹하기 때문입니다.
이걸 Head-of-Line Blocking(HOL Blocking)이라고 합니다.
QUIC는 UDP 위에 구현되었습니다. 그런데 UDP의 단점(신뢰성 부족)을 해결하기 위해, 애플리케이션 레벨에서 TCP 기능을 재구현했습니다.
핵심 차이는 "스트림 독립성"입니다. 이미지 1번이 유실돼도, 이미지 2~10번은 독립적으로 처리됩니다. 1번만 재전송받고, 나머지는 바로 화면에 표시됩니다.
추가로 QUIC는:
제가 QUIC를 이해하고 나서 든 생각은, "결국 미래는 UDP다"였습니다. TCP의 신뢰성은 필요하지만, TCP의 경직된 구조는 현대 웹에 맞지 않습니다. QUIC는 "UDP의 자유도 + 직접 구현한 신뢰성"으로 TCP를 뛰어넘었습니다.
현재 YouTube, Google, Facebook이 모두 QUIC를 씁니다. 인터넷 트래픽의 상당 부분이 이미 UDP 기반입니다.
| 구분 | TCP | UDP | QUIC |
|---|---|---|---|
| 연결 | 연결 지향 (3-Way Handshake) | 비연결 | 연결 지향 (0-RTT 가능) |
| 신뢰성 | 완벽 (재전송, 순서 보장) | 없음 | 완벽 (스트림 독립) |
| 속도 | 느림 | 매우 빠름 | 빠름 |
| 헤더 | 20~60바이트 | 8바이트 | 가변 (암호화 포함) |
| HOL Blocking | 있음 | 없음 | 없음 |
| Nagle | 있음 (TCP_NODELAY로 끌 수 있음) | 해당 없음 | 없음 |
| NAT 통과 | 어려움 | 쉬움 (Hole Punching) | 쉬움 |
| 암호화 | TLS 별도 필요 | 없음 | 내장 (TLS 1.3) |
| 대표 서비스 | HTTP/1.1, HTTP/2, SSH, FTP | DNS, 게임, WebRTC | HTTP/3, YouTube |
TCP와 UDP를 이해하기까지 저는 수많은 글을 읽었고, 직접 코드를 짜봤고, 디버깅을 했습니다. 처음엔 "TCP는 신뢰성, UDP는 속도"라는 한 줄 요약만 외웠는데, 이제는 왜 그런지 이해했습니다.
제가 받아들인 핵심은 이겁니다:
결국 네트워크 프로그래밍은 "트레이드오프를 이해하고 선택하는 것"이었습니다. 채팅 앱에는 TCP, 게임에는 UDP, 웹 서비스에는 QUIC. 이제 저는 프로젝트마다 어떤 프로토콜을 쓸지 자신있게 선택할 수 있습니다.
그리고 다음번엔 직접 UDP 위에 커스텀 신뢰성 프로토콜을 만들어볼 생각입니다. QUIC가 한 걸 제 손으로 구현해보고 싶습니다. 그게 진짜 이해하는 길이니까요.
My first project was a simple real-time chat application. I learned socket programming in Python, spun up a server, and tested it. Messages went through. Success! "It works!" I proudly showed it to friends.
Then one friend asked: "Are you using TCP or UDP?"
I froze.
I knew I used SOCK_STREAM, which meant TCP, but I had no idea why TCP, or what would change if I used UDP.
I just followed a tutorial blindly.
That night, I dove into dozens of blog posts trying to understand the difference between TCP and UDP. I memorized "TCP is reliable, UDP is fast," but I couldn't grasp how TCP guarantees reliability or why UDP is faster. Terms like "3-Way Handshake," "Sliding Window," and "Nagle's Algorithm" were complete mysteries.
Then I found one analogy that clicked: "TCP is registered mail, UDP is a postcard."
Registered mail requires a signature from the recipient, and if undelivered, it's resent. A postcard is just dropped in a mailbox. That image crystallized everything in my head.
What I finally understood: "TCP is 'a delivery driver waiting at your door', UDP is 'tossing it in the mailbox and leaving.'" This post is my organized notes from that journey of understanding.
Initially, I thought TCP was just "more reliable." But when I ran performance tests, TCP was 2-3x slower than UDP.
Why? TCP requires something called a 3-Way Handshake before sending data. When I first heard the term, I thought "it's just a greeting," but it actually involves 3 network round trips.
TCP must establish a connection before transmitting data. This connection process is the 3-Way Handshake.
Only after these 3 steps can data be sent. On a fast network, this takes milliseconds. On an overseas server, it can take hundreds of milliseconds.
My first reaction was, "Why is this necessary? Just send the data!" But I learned this process is the only way to confirm the other side is alive and ready to receive.
The 3-Way Handshake creates a security vulnerability. Attackers can send millions of SYN packets and disappear, a technique called SYN Flood.
The server sends "SYN-ACK" and waits for the client's "ACK." If the ACK never comes, the server keeps the connection info in memory. If attackers abuse this, server memory explodes.
The defense is SYN Cookies. The server doesn't store connection info in memory. Instead, it creates an encrypted cookie and sends it back. When I learned this, I realized: "Ah, TCP accepts this overhead to guarantee reliability."
The moment I truly understood TCP vs UDP was when I encountered this metaphor:
"TCP is a Butler, UDP is a Courier."A butler brings you water and asks, "Did you receive it?" If you didn't get it, they bring it again. A courier just tosses the package at your door and leaves. No confirmation.
This resonated because my chat app was a service where losing even one message is unacceptable. If my "Hello" message doesn't arrive, the other person might think I'm ignoring them. That's why I needed TCP.
On the other hand, if I were building a real-time game, I'd use UDP. In a game, if a "character position (x=100, y=200)" packet is lost, it's fine—another packet "(x=105, y=205)" arrives 0.1 seconds later. No need to wait for old packets.
TCP guarantees reliability through 4 main mechanisms.
TCP assigns a Sequence Number to every packet. For example, sending "Hello World":
If packets arrive out of order (2 → 1), the receiver reassembles them (1 → 2). This is why text on websites never gets scrambled.
UDP has no such feature. Packets are processed in arrival order.
After sending a packet, TCP waits for an ACK (Acknowledgment). If the ACK doesn't arrive, TCP assumes the packet was lost and retransmits it.
To understand this, I ran an experiment.
I used a network delay simulator (tc command) to set a 30% packet loss rate.
TCP automatically retransmitted and delivered 100% of the data.
UDP simply lost 30% of the data.
Retransmission relies on Timeout mechanisms. TCP dynamically calculates "how long to wait" based on RTT (Round Trip Time). If the network is fast, it waits briefly. If slow, it waits longer.
When I understood this, I realized: "TCP doesn't just retransmit—it learns the network conditions in real-time."
If the receiver's processing speed is slower than the sender's transmission speed, the buffer overflows. TCP prevents this using the Sliding Window technique.
The receiver says, "I can only handle 1024 bytes right now (Window Size = 1024)," and the sender limits itself to that. When the buffer clears, the Window Size grows. When it fills, it shrinks.
When I first learned this, I thought, "Why make it so complicated?" But when dozens of people connected to my chat server simultaneously, the server couldn't handle it and crashed. I experienced firsthand what happens without flow control.
Flow control solves the receiver's problem, but congestion control solves the entire network's problem.
The internet is a shared resource. If everyone transmits at maximum speed simultaneously, routers collapse. TCP prevents this using the Slow Start algorithm.
It starts by sending 1 packet. If the ACK arrives, it sends 2, then 4, then 8... exponentially increasing. When packet loss occurs, TCP says, "Ah, the network is congested," and drastically reduces speed.
This mechanism ensures fairness. All TCP connections share network bandwidth equally. UDP has no such mechanism, so heavy UDP traffic can starve TCP connections.
UDP removes all of TCP's features.
It's simply "create a packet and throw it." That's it.
UDP's philosophy is: "If you need reliability, implement it yourself at the application level." This confused me at first. "Why not just use TCP for everything?"
But then the reasons for UDP's necessity became clear:
I truly understood UDP when I tried implementing P2P video calling and hit the NAT problem.
A computer behind a home router (NAT) cannot be directly accessed from outside. The router shares one external IP across multiple devices.
TCP struggles with this problem. "Client → Server" works, but "Client ↔ Client" doesn't.
UDP solves this with Hole Punching:
WebRTC (video calls) and online games all use this technique. After understanding this, my thinking shifted: "UDP isn't just simple—it's flexible."
The strangest bug I encountered while building my real-time chat app was "messages occasionally delayed by 0.2 seconds."
Debugging revealed that the code sending the message executed instantly, but the actual packet transmission happened 0.2 seconds later. The culprit was Nagle's Algorithm.
In the 1980s, the internet was slow. Sending thousands of tiny packets clogged the network. Nagle's Algorithm was created to solve this.
"Aggregate small data and send it in bulk."For example, instead of sending "H", "e", "l", "l", "o" 5 times (1 byte each), wait 0.2 seconds and send "Hello" (5 bytes) once. Network efficiency improves, but latency increases.
For games or real-time chat, "0.2 seconds of latency" is fatal. Developers use the TCP_NODELAY option to disable Nagle.
import socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Disable Nagle
With this option enabled, even 1-byte packets are sent immediately. Network load increases slightly, but responsiveness improves dramatically.
After enabling this option, the "sluggishness" of my chat app vanished. I realized: "Using TCP requires understanding these kinds of details."
The difference between TCP and UDP becomes crystal clear when you look at their headers.
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Sequence Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Acknowledgment Number |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Data | |U|A|P|R|S|F| |
| Offset| Reserved |R|C|S|S|Y|I| Window |
| | |G|K|H|T|N|N| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Checksum | Urgent Pointer |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Key Fields:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Source Port | Destination Port |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length | Checksum |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
That's it. Port numbers, length, checksum. Done.
After seeing these headers, I understood "why UDP is fast." TCP requires significant CPU processing just for header management. UDP barely touches the header.
Theory alone didn't resonate, so I wrote code myself.
import socket
# Create TCP server
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) # Disable Nagle
server.bind(('0.0.0.0', 9000))
server.listen(5)
print("TCP server listening...")
while True:
conn, addr = server.accept() # 3-Way Handshake happens here
print(f"Connected: {addr}")
while True:
data = conn.recv(1024)
if not data:
break
print(f"Received: {data.decode()}")
conn.send(data) # Echo
conn.close()
Features:
accept() triggers the 3-Way Handshake.recv() receives data in order.data is empty, causing break.import socket
# Create UDP server
server = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
server.bind(('0.0.0.0', 9000))
print("UDP server listening...")
while True:
data, addr = server.recvfrom(1024) # Receive without connection
print(f"Log from {addr}: {data.decode()}")
# No response needed (Fire and Forget)
Features:
accept(). No connection concept.recvfrom() receives a packet along with the sender's address.TCP Client:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(('localhost', 9000)) # 3-Way Handshake
client.send(b"Hello TCP")
response = client.recv(1024)
print(response)
client.close()
UDP Client:
import socket
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
client.sendto(b"Hello UDP", ('localhost', 9000)) # Just throw it
# If you want a response: data, addr = client.recvfrom(1024)
After writing this code, the metaphor clicked: "TCP is a phone call (connection-based), UDP is a walkie-talkie (broadcast)."
Here's how I choose between TCP and UDP for each project.
After organizing these criteria, I realized: "TCP isn't universally better—it depends on the situation."
Google's recent QUIC protocol became the web standard (HTTP/3). What shocked me was that it abandoned TCP.
When TCP loses packet #1, packets #2, #3, #4 (which have already arrived) wait. They must maintain order.
When loading a webpage with 10 images simultaneously, if packet #1 is lost, images #2-10 don't display even though they've all arrived. TCP blocks everything until #1 is retransmitted.
This is called Head-of-Line Blocking (HOL Blocking).
QUIC is built on UDP. However, to solve UDP's shortcomings (lack of reliability), it reimplements TCP features at the application layer.
The key difference is "stream independence." If image #1 is lost, images #2-10 are processed independently. Only #1 is retransmitted; the rest display immediately.
Additionally, QUIC offers:
After understanding QUIC, my takeaway was: "The future is UDP." TCP's reliability is necessary, but TCP's rigid structure doesn't fit the modern web. QUIC surpasses TCP with "UDP's flexibility + custom-built reliability."
Currently, YouTube, Google, Facebook all use QUIC. A significant portion of internet traffic is already UDP-based.
| Feature | TCP | UDP | QUIC |
|---|---|---|---|
| Connection | Connection-oriented (3-Way Handshake) | Connectionless | Connection-oriented (0-RTT possible) |
| Reliability | Perfect (retransmission, ordering) | None | Perfect (independent streams) |
| Speed | Slow | Very Fast | Fast |
| Header | 20-60 bytes | 8 bytes | Variable (encryption included) |
| HOL Blocking | Yes | No | No |
| Nagle | Yes (disable with TCP_NODELAY) | N/A | No |
| NAT Traversal | Difficult | Easy (Hole Punching) | Easy |
| Encryption | Requires separate TLS | None | Built-in (TLS 1.3) |
| Examples | HTTP/1.1, HTTP/2, SSH, FTP | DNS, Games, WebRTC | HTTP/3, YouTube |