What This Article Is About
Every byte of internet traffic is wrapped in either TCP or UDP. HTTP, video calls, gaming, email, DNS, streaming, SSH, every database connection: all of them sit on top of one of these two transport protocols. Picking which one to use, or just understanding which one your stack picked for you, is one of the most foundational engineering decisions in networking.
The high-level pitch is "TCP is reliable, UDP is fast" and that's not wrong. But it hides a lot. TCP being "reliable" means specific things (retransmissions, ordering, flow control, congestion control) that each cost specific amounts of latency. UDP being "fast" means giving up everything TCP does, which is fine in some cases and disastrous in others. And QUIC, the protocol behind HTTP/3, takes the best of both and reshuffles the trade-offs.
This article walks through how TCP actually works under the hood, why UDP exists, what each is good for, where the boundary blurs, and how to make the right call.
The Layered Mental Model
Networking has layers. You don't have to memorize the OSI model, but you do need to know:
Layer 3 (Network): IP. Routes packets between hosts. Doesn't care about reliability or order. "Send this packet to 1.2.3.4. Hopefully it gets there."
Layer 4 (Transport): TCP or UDP. Sits on top of IP. Provides the abstraction your application uses (a stream of bytes for TCP, individual packets for UDP).
Layer 7 (Application): HTTP, gRPC, SMTP, etc. Sits on top of TCP (mostly) or UDP (DNS, video, etc.).
TCP and UDP are sibling protocols at the same level. Both are built on IP. They take very different approaches to the same problem: turning unreliable per-packet IP into something useful for an application.
The Core Difference in One Paragraph
TCP guarantees that every byte you send arrives at the other end, in order, exactly once. If a packet is lost, TCP detects this and retransmits. If packets arrive out of order, TCP reorders them. The application sees a clean stream of bytes.
UDP sends individual packets and forgets about them. No retransmission, no ordering, no acknowledgement. If a packet is lost, it's gone. If packets arrive out of order, the application sees them out of order. The application sees individual messages, not a stream.
Why would you ever want UDP? Because TCP's guarantees cost time. Sometimes "best effort, fast" beats "perfect, slow".
How TCP Actually Works
TCP is a small operating system in your kernel. It does a lot to deliver "reliable, ordered byte stream" on top of unreliable IP packets.
The Three-Way Handshake
Before any data flows, client and server set up a connection through three packets:
SYN: client picks an initial sequence number x, sends "I want to talk".
SYN-ACK: server picks its own initial sequence number y, acknowledges x+1, sends back.
ACK: client acknowledges y+1. Now both sides agree on starting sequence numbers.
Cost: 1 round trip before any application data flows. On a 100ms RTT link, that's 100ms of dead time before your first byte.
Sequence Numbers and ACKs
Every byte gets a number. The receiver sends ACKs telling the sender "I've received everything up to byte N". If the sender doesn't receive an ACK in time, it retransmits.
Modern TCP uses Selective Acknowledgement (SACK): the receiver can say "I have bytes 0 to 1000 and 2000 to 3000, but not 1001 to 1999". The sender retransmits only the missing chunk.
Flow Control (Don't Overwhelm the Receiver)
The receiver advertises a "window size" telling the sender how much unacknowledged data it can have in flight at once. If the receiver's buffer fills, it shrinks the window. If the receiver is fast and idle, it grows it.
This is "sliding window". It prevents fast senders from drowning slow receivers.
Congestion Control (Don't Overwhelm the Network)
Different from flow control. Flow control is about the receiver. Congestion control is about the network in between.
TCP infers network congestion from packet loss. If packets are being lost, the network is probably congested, slow down. If packets are flowing, speed up.
Algorithms (these matter for performance):
Reno: the classic. Halves the sending rate on packet loss, slowly increases when things are fine. Still common.
Cubic: default in Linux for years. Faster recovery on high-bandwidth networks.
BBR: Google's. Models the network's bandwidth-delay product directly instead of reacting to loss. Performs much better on lossy or bufferbloated networks. Increasingly the default.
You usually don't pick the algorithm. The OS does. But knowing they exist helps explain why two TCP connections on the same network can perform very differently.
Ordered Delivery (And Head-of-Line Blocking)
TCP delivers bytes in order. If packet 5 is lost but packets 6, 7, 8 arrive, the receiver buffers them and waits for 5 to be retransmitted. The application doesn't see anything until 5 arrives.
This is fine when the application needs strict order. But for applications multiplexing multiple independent streams over a single TCP connection (like HTTP/2), one lost packet on stream A blocks delivery of unrelated data on stream B. This is the famous head-of-line blocking problem that QUIC solves.
The Connection Teardown
Either side can close. The clean close is a four-way exchange (FIN, ACK, FIN, ACK). The half-close (one side done writing, the other still reading) is sometimes used. There's also the abrupt RST close, which is faster but doesn't ensure all data is delivered.
How UDP Actually Works
UDP is almost nothing. The header has four fields:
Source port (16 bits)
Destination port (16 bits)
Length (16 bits)
Checksum (16 bits)
Eight bytes total. That's it. Compare to TCP's 20+ bytes (40+ with options).
What you give the kernel: a destination IP, port, and some bytes. What it does: wraps it in a UDP header, hands it to IP, IP hands it to the network. Done.
What you get:
Lower latency (no handshake, no waiting for ACKs).
Lower per-packet overhead.
No connection state to maintain on either side.
Multicast and broadcast (TCP can't do these).
Predictable latency (no congestion control kicking in unpredictably).
What you don't get:
Any guarantee that data arrives.
Any guarantee about order.
Any flow control or congestion control.
Reliability under packet loss (you lose data without knowing).
If your app needs reliability, UDP gives you the bricks. You build the house yourself.
Side-by-Side Comparison
| TCP | UDP | |
|---|---|---|
| Reliability | Guaranteed | None |
| Ordering | Guaranteed | None |
| Connection | Yes (handshake required) | No (packets are independent) |
| Initial latency | 1 RTT before data | 0 RTT |
| Header size | 20+ bytes | 8 bytes |
| Flow control | Built in | None |
| Congestion control | Built in | None |
| Multicast | No | Yes |
| Stream model | Byte stream | Discrete messages |
| NAT friendliness | Excellent | Trickier |
| Common use cases | Web, email, files, DBs | DNS, video, games, VoIP |
Where TCP Wins
HTTP/HTTPS: web pages, API calls, almost everything you think of as "web traffic". Until QUIC arrived in 2022, all HTTP was TCP. Even today, HTTP/1.1 and HTTP/2 are TCP-based.
Email (SMTP, IMAP, POP3): losing parts of an email is unacceptable. TCP guarantees what the user expects.
File transfer (FTP, SCP, rsync): need every byte. Order matters.
Database connections (Postgres, MySQL, Redis): can't drop a query mid-execution. State per connection.
SSH: reliability matters more than the few hundred ms of handshake overhead.
Most application-layer messaging: Kafka, gRPC (HTTP/2), WebSocket (over HTTP/HTTPS). Built on TCP because they need reliability.
Where UDP Wins
DNS: queries are tiny, latency matters, retries are cheap. UDP gives you sub-millisecond when cached. TCP fallback exists for big responses.
Video calls (Zoom, Google Meet, FaceTime, WebRTC): if a packet of audio is lost, retransmitting it 200ms later is useless, the conversation has moved on. Just skip it. Real-time matters more than completeness. The codec handles small losses gracefully.
Online gaming: same logic. A position update from 500ms ago is irrelevant. Game clients use UDP and handle their own ordering/reliability where needed.
Live streaming (some protocols): SRT, RIST, parts of WebRTC. Low-latency live streaming uses UDP. Pre-recorded streaming (Netflix, YouTube VOD) typically uses TCP-based HTTP for adaptive bitrate.
VoIP (RTP): voice packets can drop without ruining the call. Most VoIP rides on UDP.
VPNs: WireGuard uses UDP for low overhead. OpenVPN can do either; UDP is the recommended default.
Multicast applications: stock market feeds, video distribution within a network, some discovery protocols (mDNS, SSDP). TCP literally cannot do multicast.
QUIC and HTTP/3: ride on UDP for reasons we'll cover.
Why Real-Time Apps Hate TCP
Imagine a video call with one packet loss. Here's what TCP does:
1. Receiver notices the gap.
2. Sends duplicate ACKs to signal the loss.
3. Sender retransmits.
4. Meanwhile, all packets after the lost one are buffered (head-of-line blocked).
5. After retransmit succeeds, all the buffered packets get delivered at once.
From the user's perspective: the call freezes for 200ms, then 200ms of audio plays back at once. That's much worse than just losing the lost packet and continuing.
UDP just delivers what arrived. The codec interpolates over the missing packet. The call sounds slightly worse for 20ms. The user might not even notice.
The Hybrid Reality: QUIC
For decades, "TCP for reliable, UDP for fast" was the simple rule. Then QUIC arrived and broke it.
QUIC is a new transport protocol that runs on top of UDP. It re-implements (in user space, not the kernel) most of what TCP does well, while leaving out what TCP does badly.
What QUIC keeps:
Reliability (retransmissions).
Ordering (within a stream).
Congestion control.
Flow control.
What QUIC adds or improves:
Multiple independent streams per connection (no head-of-line blocking across streams).
Built-in TLS 1.3 (always encrypted, integrated with the handshake).
Faster handshake (0 or 1 RTT vs TCP+TLS's 2-3 RTT).
Connection migration (survives switching from WiFi to cellular).
Evolves in user space, not in the kernel (browsers and servers can ship updates without OS changes).
Why on UDP? Because deploying a brand-new transport protocol on the internet is impossible. Every NAT, firewall, and router along the path would need to learn it, which would take decades. UDP packets, on the other hand, get through pretty much everywhere already. So QUIC pretends to be UDP at the wire level and does its own thing inside.
HTTP/3 runs on QUIC. So do increasing numbers of other protocols.
NAT, Firewalls, and Why UDP Is Trickier
Network Address Translation (NAT) lets multiple devices share one public IP. The NAT box maintains a mapping table: when you send a packet from 192.168.1.5:5000, it rewrites the source to your-public-ip:54321 and remembers the mapping so it can deliver replies back.
For TCP, this is straightforward. The connection has explicit setup (SYN), explicit teardown (FIN), and known states. The NAT box knows when to expire the mapping.
For UDP, there's no connection. The NAT box has to guess. Most NATs use a timeout (often 30 seconds for UDP). If you go quiet for 30 seconds, the mapping is gone and replies don't reach you anymore.
This means UDP apps need to send keepalive packets (just to keep NAT mappings alive). It also means peer-to-peer UDP requires "hole punching" (both sides send packets simultaneously to each other's NAT-translated address; this opens mappings on both sides). WebRTC, gaming, and VoIP all do this.
TCP is much more boring at this layer. It just works.
How Applications Actually Choose
Default: TCP. If you don't have a specific reason to pick UDP, use TCP. Every higher-level protocol you'd reach for (HTTP, WebSockets, gRPC, message queues) sits on TCP for good reasons.
Pick UDP when:
Latency is critical.
Lost packets are tolerable because newer data makes old data obsolete (real-time media, gaming, telemetry).
You need multicast or broadcast.
You're building a custom transport (QUIC, custom game protocol).
If you reach for UDP, understand that you're committing to handle reliability and ordering yourself if you need them. That's a lot of work. Consider QUIC instead, which gives you a sane middle ground.
Performance Numbers (Roughly)
For a request-response interaction (small data, single round trip) on a 100ms RTT link:
UDP: ~100ms (the round trip itself).
TCP: ~200ms (handshake + round trip).
TCP+TLS 1.3: ~300ms (handshake + TLS + round trip).
TCP+TLS 1.2: ~400ms.
QUIC fresh: ~200ms (handshake includes TLS).
QUIC resumed (0-RTT): ~100ms.
For bulk transfers (lots of data, congestion control matters):
TCP with BBR or Cubic on a healthy link: typically 80-95% of bottleneck bandwidth.
TCP on lossy links: degrades fast. 1% packet loss can halve throughput.
UDP raw: full bandwidth, no congestion control means you can also kill the network.
QUIC on lossy links: noticeably better than TCP because no head-of-line blocking and modern congestion control.
The QUIC benefits are biggest on mobile, where networks are lossy and connections migrate.
Edge Cases and Gotchas
The "thundering herd" problem with UDP. Without congestion control, a UDP application can flood a network it shares with TCP traffic. The TCP traffic backs off (because it sees loss). The UDP traffic doesn't. Result: UDP eats the network. This is why responsible UDP apps implement their own congestion control.
Connection state and scaling. Each TCP connection occupies kernel memory (typically a few KB) and a port number. Servers running 100k concurrent TCP connections is normal but requires tuning. UDP has no such state, so a "connection" is just an entry in your application's data structures.
Firewalls and corporate networks. Some networks block UDP except for DNS. This breaks things like WebRTC and QUIC. Most browsers fall back to TCP-based HTTP/2 if QUIC fails.
Long-lived TCP connections and TIME_WAIT. When you close a TCP connection, the kernel keeps the connection in TIME_WAIT state for a couple of minutes (to handle stragglers). High-throughput services can run out of available source ports. Tune tcp_tw_reuse and friends, or use connection pooling.
Nagle's algorithm. TCP buffers small writes for efficiency (sends them in larger chunks). For interactive applications (SSH, real-time control), this adds latency. Disable with TCP_NODELAY.
Slow start. TCP starts slow (small congestion window) and ramps up. For short-lived connections (one HTTP request), you might never reach full bandwidth. HTTP/2 and HTTP/3 help by reusing connections.
UDP fragmentation. If a UDP packet is bigger than the MTU (typically ~1500 bytes), it gets fragmented. Some networks drop fragments. Stick to ~1200 bytes for UDP payloads to be safe (this is what QUIC does).
Bufferbloat. Routers with large buffers absorb temporary loss without telling TCP, but cause huge latency. TCP doesn't realize it's overshooting until very late. Modern algorithms (BBR, CoDel queueing) help.
How to Actually Look at TCP/UDP Traffic
tcpdump: command-line packet capture. tcpdump -i eth0 port 443 dumps all traffic on port 443. Steep learning curve but powerful.
Wireshark: graphical version. Easier to learn. Shows every flag, every option, every retransmission.
ss / netstat: see current connections. ss -tan shows TCP connections and states.
tc / qdisc: Linux traffic control. Shape, throttle, simulate loss for testing.
iperf3: measure raw TCP/UDP throughput between two endpoints.
Most TCP issues are visible in a packet capture as retransmissions, duplicate ACKs, or window size collapses. If you're debugging slow networking, learn enough Wireshark to spot these.
Don't Panic: Application Frameworks Hide Almost All of This
If you write web apps, you mostly don't deal with TCP directly. You use a framework that uses an HTTP server that uses sockets that use TCP. The kernel handles handshakes, retransmissions, and congestion control on your behalf.
Knowing the underlying mechanics is still useful because:
It explains why latency, throughput, and connection limits behave the way they do.
It lets you read packet captures and diagnose weird production issues.
It informs decisions about timeouts, keep-alives, connection pooling, retry strategies.
It makes the difference between "ssh feels slow today" and "let me check if I'm seeing TCP retransmissions on this link".
The One Thing to Remember
TCP guarantees in-order, reliable delivery at the cost of latency. UDP gives you raw speed at the cost of reliability. The right choice depends on whether your application would rather wait for missing data or skip it. Most things use TCP because most things would be broken without it. Specialized real-time systems use UDP and rebuild the bits of TCP they need on top. QUIC is the modern answer for systems that want both: reliability without head-of-line blocking, encryption without extra round trips, and the freedom to evolve outside the kernel. Knowing the mechanics, even at a high level, makes you a much better debugger of network weirdness.