TCP vs UDP
TCP vs UDP for system design interviews: the three-way handshake, sequence numbers and retransmission, flow vs congestion control, head-of-line blocking, TIME_WAIT, UDP's 8-byte header, QUIC and HTTP/3, connection pooling in round trips, and which transport to pick for video calls, games, DNS, chat and payments.
On this page
- Picture it first: a TCP connection's whole life
- What TCP gives you, and what each promise costs
- UDP: 8 bytes and a promise of nothing
- QUIC: UDP plus reliability in user space
- Connection reuse: the cheapest latency win
- When to pick each
- In Java, briefly
- Trade-offs and failure modes
- How it shows up in design problems
- Follow-up questions
- How to say it in the interview
🚚 What are TCP and UDP?
TCP and UDP are the two transport protocols almost every app sits on. TCP gives you a connection and a reliable, ordered stream of bytes: it numbers everything, waits for acknowledgements and resends what got lost. UDP gives you single packets (datagrams) with a port number and a checksum, and nothing else: no connection, no resends, no ordering. TCP is a tracked courier that gets a signature for every parcel; UDP is dropping postcards in a box.
For this lesson, we'll cover what TCP actually does for you (handshake, acknowledgements, ordering, flow and congestion control, teardown), what UDP leaves out, why QUIC rebuilt TCP's job on top of UDP, and how to pick one in a design. Interviewers ask it because "TCP is reliable, UDP is fast" is only half an answer; the interesting part is what reliability costs and when an app is better off without it.
Below the line (out of scope)
- Where these sit in the stack: The OSI model
- Addresses and ports below them: IP addresses
- What runs on top of TCP and QUIC: HTTP and HTTPS
- The checksum both protocols carry: Checksums
The 30-second answer
TCP is connection-oriented: a three-way handshake costs one round trip, then it delivers a reliable, ordered byte stream using sequence numbers, acknowledgements and retransmission, with flow control so the receiver isn't flooded and congestion control so the network isn't. The price is setup latency and head-of-line blocking: one lost packet stalls everything behind it. UDP has an 8-byte header with ports, length and a checksum, and no delivery or ordering guarantees, so the app decides what to rebuild. I pick TCP for anything where every byte matters (APIs, payments, chat, file transfer) and UDP where late data is useless (voice, video, game state) or the exchange is one tiny question (DNS). QUIC, which carries HTTP/3, is the middle path: TCP-style reliability and TLS 1.3 built in user space over UDP, with independent streams and a 1-RTT setup.
What you’ll learn
- Draw the three-way handshake and teardown, and explain why the side that closes first sits in TIME_WAIT
- Explain how TCP gets reliability and ordering from sequence numbers, ACKs and retransmission, and what head-of-line blocking costs
- Tell flow control (protects the receiver) from congestion control (protects the network) in one sentence
- Say what UDP's 8-byte header gives you and what apps rebuild on top of it
- Describe QUIC as reliability and TLS 1.3 in user space over UDP, and when HTTP/3 helps
- Cost a fresh vs pooled HTTPS connection in round trips and pick a transport per use case
01Picture it first: a TCP connection's whole life
The fastest way to understand TCP is to watch one connection from start to finish. Every promise TCP makes shows up as a message in this picture.
A TCP connection from SYN to TIME_WAIT
Notice four things:
- The handshake is a round trip of pure overhead. SYN, SYN-ACK, ACK agrees on starting sequence numbers before any data moves. The client's request can ride on the final ACK, so setup costs one RTT, not one and a half.
- ACKs are cumulative. "ack=x+1001" means "I have everything up to byte 1000, send 1001 next". The sender doesn't need a receipt per segment.
- Loss is repaired by the sender. It notices either because a timer expires (RFC 6298 starts it at 1 second before any RTT is measured) or because it gets three duplicate ACKs for the same byte, which triggers a fast retransmit (RFC 5681).
- Closing is a two-way goodbye. Each direction sends its own FIN, so a connection can be half-closed for a moment.
02What TCP gives you, and what each promise costs
| Promise | Mechanism | What it costs |
|---|---|---|
| Connection | Three-way handshake, state on both ends | 1 RTT before data; memory per connection |
| Reliability | Sequence numbers, ACKs, retransmission | Extra round trips on loss; latency spikes |
| Ordering | Receiver buffers out-of-order bytes until the gap fills | Head-of-line blocking |
| Flow control | Receiver advertises a window: "I have room for N bytes" | A slow reader throttles a fast sender |
| Congestion control | Sender's congestion window grows until loss or delay says stop | New connections start slow |
| Integrity | 16-bit checksum over header and data | Catches some corruption, not all (see Checksums) |
Flow control vs congestion control
The one-line split I use: flow control protects the receiver, congestion control protects the network.
- Flow control. Every ACK carries a receive window: the free space in the receiver's buffer. If your consumer reads slowly, the window shrinks to zero and the sender pauses. That's TCP doing backpressure for you, the same idea as backpressure between services.
- Congestion control. The sender also keeps its own congestion window, a guess at what the path can carry. It starts small, roughly doubles every round trip (slow start), then grows cautiously and cuts back when it sees loss. RFC 6928 raised the starting window to 10 segments, about 14 KB with typical 1,460-byte segments, which is why a fresh connection can't send a 1 MB response in its first round trip.
The sender may have whichever window is smaller in flight. In an interview, the point is that a brand-new connection is slow on purpose, which is a big reason to reuse connections.
Head-of-line blocking
TCP hands bytes to the application strictly in order. If segment 5 is lost and 6 to 20 arrive fine, the app sees nothing past 4 until 5 is resent. For a protocol that multiplexes many requests over one connection, like HTTP/2, one lost packet stalls every request, which is the main problem QUIC was built to fix.
One lost packet: TCP stalls every stream, QUIC stalls one
Teardown and TIME_WAIT
After the last ACK, the side that closed first parks the connection in TIME_WAIT for twice the maximum segment lifetime. RFC 9293 takes the MSL as 2 minutes, so 4 minutes on paper; Linux hard-codes 60 seconds. The wait makes sure stray segments from the old connection die before a new one reuses the same address and port pair.
Here's why that matters in a design. A client making many short connections to one destination burns an ephemeral port per connection. Linux's default ephemeral range is 32768 to 60999, about 28,000 ports, so at 60 seconds of TIME_WAIT that's roughly 470 new connections a second to one destination IP and port before you run out. The fix is almost never kernel tuning; it's connection pooling.
03UDP: 8 bytes and a promise of nothing
UDP (RFC 768) adds four 16-bit fields to an IP packet and stops there:
0 16 31
+---------------+---------------+
| source port | dest port |
+---------------+---------------+
| length | checksum |
+---------------+---------------+
| data (one datagram) ... |
No handshake, no ACKs, no ordering, no congestion control, no connection state. Each datagram arrives once, never, or (rarely) twice or out of order. The checksum is optional over IPv4 and mandatory over IPv6 (RFC 8200). Apps that pick UDP rebuild only the parts they need:
| Need | What UDP apps typically add |
|---|---|
| Match a reply to a request | An ID in the payload (DNS uses a 16-bit query ID) |
| Detect loss | Sequence numbers per packet (RTP for media does this) |
| Recover loss | Selective resend, or forward error correction, or "skip it" |
| Ordering | Reorder buffer with a deadline, then play what you have |
| Don't flood the network | Rate control based on measured loss or delay |
| Security | DTLS, or QUIC's built-in TLS 1.3 |
The pattern: a voice call wants loss detection (to conceal a gap) but not recovery, because a packet that arrives 300 ms late is worse than no packet. TCP can't give you that; it will always wait for the resend.
04QUIC: UDP plus reliability in user space
QUIC (RFC 9000) is a full transport, with streams, ACKs, retransmission and congestion control, built on top of UDP and shipped inside the application or library instead of the OS kernel. HTTP/3 (RFC 9114) runs on it. Three things matter for interviews:
- Independent streams. Loss on one stream doesn't block the others, which removes TCP's head-of-line blocking.
- Faster setup. QUIC does the transport and TLS 1.3 handshakes together in one round trip, and can send data in the first flight (0-RTT) when resuming. Encryption isn't optional.
- Connection IDs, not addresses. A QUIC connection is identified by an ID, so a phone moving from Wi-Fi to cellular can keep the connection instead of reconnecting (RFC 9000 calls this connection migration).
Why not fix TCP instead? TCP lives in every OS kernel and middleboxes inspect it, so changing it takes years. UDP gets through almost everywhere, and logic in user space ships with the next app release. The figure lines the two stacks up.
Same job, two stacks: what each layer costs in round trips
Uber's rider and driver apps run on lossy mobile networks in hundreds of cities. When their engineers traced production TCP connections from India, they saw tail RTTs close to six times the median and TCP retransmitting about 3.5% of packets, up to 7% in crowded places like railway stations. After moving the apps' HTTPS traffic to QUIC, they reported 10 to 30% lower tail latency at scale. The lesson I take from it: on clean data-centre links TCP is fine, but on the last mile to a phone, head-of-line blocking and kernel-bound loss recovery show up directly in p99.
05Connection reuse: the cheapest latency win
Every new HTTPS connection over TCP pays a TCP handshake, then a TLS handshake, then the request itself. Reusing a connection (HTTP keep-alive, a client-side connection pool, HTTP/2 multiplexing) skips the first two entirely.
Try it yourself first
Before reading on, work this out. Your service in Mumbai calls a payment provider in Virginia, about 200 ms round trip. Each call opens a fresh HTTPS connection with TLS 1.3. How long before the first byte of the response arrives? What if the connection is pooled? And what if you made 5 calls in a row?
Here's how I'd do it out loud (ignoring server processing time):
| Step | Fresh connection | Pooled connection |
|---|---|---|
| TCP handshake | 1 RTT = 200 ms | 0 |
| TLS 1.3 handshake | 1 RTT = 200 ms | 0 |
| Request and response | 1 RTT = 200 ms | 1 RTT = 200 ms |
| One call | about 600 ms | about 200 ms |
| Five sequential calls | about 3 s | about 1 s |
With TLS 1.2 it's one more round trip per new connection. Notice which number drives the design: two thirds of the fresh-connection time is setup. Pooling, keep-alive and putting a TLS-terminating proxy close to the caller all attack that, which is also why CDNs terminate TLS near users (see CDN).
Slow start adds to the bill on a cold connection. With an initial window of 10 segments of 1,460 bytes, the server can send about 14.6 KB in the first round trip, then roughly 29 KB, then 58 KB as the window doubles. So a 100 KB response needs about 3 round trips on a fresh connection, versus 1 on a warm one whose window has already grown (our own rough arithmetic, ignoring loss). Inside one data centre the RTT is tiny (see Numbers to know), so I care most about pooling on cross-region and internet-facing calls.
06When to pick each
This is the table I'd sketch.
| Use case | Pick | Why |
|---|---|---|
| Payments, orders, any API | TCP (HTTP/1.1, HTTP/2) or QUIC | Every byte must arrive, in order; add idempotency for retries |
| Chat messages | TCP (WebSockets) | Order and delivery matter more than 50 ms; see real-time connections |
| Video and voice calls | UDP (WebRTC media over RTP) | A late frame is useless; conceal the gap and keep going |
| Multiplayer game state | UDP with custom reliability | Latest position wins; resend only what must arrive (a purchase, a kill) |
| DNS lookup | UDP first, TCP for large answers | One small question, one answer; no handshake worth paying for (DNS) |
| Metrics (StatsD-style) | UDP | Losing 0.1% of counter increments is fine; blocking the app isn't |
| Video on demand | TCP or QUIC (HTTP) | A buffer of several seconds hides retransmissions |
| Big file transfer | TCP or QUIC | Reliability and congestion control are exactly what you want |
Many candidates say "UDP because it's faster" for anything latency-sensitive, then quietly need every message to arrive. Choosing UDP means you now own loss detection, retransmission, ordering and congestion control, and building them badly is how you end up slower than TCP and unfair to other traffic. Pick UDP only when you can name what you'll skip (usually retransmitting stale data). If you need reliability without head-of-line blocking, the honest answer is often QUIC, not raw UDP.
07In Java, briefly
You rarely touch raw sockets in a design interview, but the API shape shows the difference.
// TCP: connect (handshake), then a reliable ordered stream
try (Socket tcp = new Socket()) {
tcp.connect(new InetSocketAddress("orders.internal", 8080), 2_000); // connect timeout, ms
tcp.setSoTimeout(3_000); // read timeout, ms
tcp.getOutputStream().write(request);
}
// UDP: one datagram, no connection, no delivery report
try (DatagramSocket udp = new DatagramSocket()) {
byte[] m = "checkout.latency:42|ms".getBytes(StandardCharsets.UTF_8);
udp.send(new DatagramPacket(m, m.length, new InetSocketAddress("metrics.internal", 8125)));
}
Notice the UDP send returns happily even if nobody is listening. Real services use a pooled HTTP client instead; HTTP and HTTPS shows one.
08Trade-offs and failure modes
| Symptom | Usually caused by | What to do |
|---|---|---|
| p99 latency spikes on mobile | Loss plus TCP retransmission and head-of-line blocking | HTTP/3 (QUIC) at the edge |
| "Cannot assign requested address" on a busy client | Ephemeral ports stuck in TIME_WAIT from short connections | Pool and reuse connections |
| First request on every call is slow | New connection: handshakes plus slow start | Keep-alive, warm pools |
| Hangs forever on a dead peer | No read timeout; Linux TCP keepalive waits 2 hours by default | Always set connect and read timeouts (fault tolerance) |
| HTTP/3 fails on some networks | UDP blocked or rate-limited by a firewall | Keep a TCP fallback; clients learn HTTP/3 via Alt-Svc |
09How it shows up in design problems
- Design Zoom: media over UDP with loss concealment, signalling and chat over TCP. Saying "two transports for two jobs" is a strong signal.
- Design WhatsApp and Slack / Discord: long-lived TCP connections (WebSockets) per device; the deep dive is connection count per server and reconnect storms.
- Design a payment system: TCP plus TLS, pooled connections to the provider, timeouts and idempotent retries.
- Design a rate limiter: the limiter sits on the hot path, so pooled connections to its counter store matter more than you'd think.
At this point, you should ask your interviewer whether clients are on mobile networks. If they are, the transport choice (and HTTP/3) becomes part of your latency story.
10Follow-up questions
What if your service starts failing with thousands of sockets in TIME_WAIT?
That's a client opening and closing short connections to the same destination, and running out of ephemeral ports (about 28,000 by default on Linux, each held for 60 seconds). I'd fix the cause: a connection pool with keep-alive so a few hundred long-lived connections carry the traffic. Kernel options that reuse TIME_WAIT sockets exist, but they treat the symptom.
What if a user's phone switches from Wi-Fi to 5G in the middle of an upload?
Over TCP the connection is tied to the old IP address and port, so it breaks and the app must reconnect, redo TCP and TLS, and resume the upload, ideally from the last acknowledged chunk. Over QUIC the connection ID survives the address change, so the transfer continues. Either way, I'd design uploads as resumable chunks so a dropped connection costs one chunk, not the whole file.
What if you need reliable messages for a game but can't afford head-of-line blocking?
Split traffic by what it needs. Position updates go unreliable and unordered, since only the latest one matters. Events that must arrive (a purchase, a match result) go on a reliable channel: either your own ACK and resend on UDP, or separate QUIC streams so one lost event doesn't freeze movement updates.
What if the interviewer pushes: "Why not run HTTP/3 between your internal services too?"
Inside a data centre, loss is low, RTTs are about a millisecond and connections are long-lived and pooled, so QUIC's wins (faster setup, no cross-stream blocking on lossy links, migration) mostly disappear. Meanwhile TCP has mature kernel offloads, load balancer support and debugging tools. I'd use HTTP/3 at the internet edge where users are, and keep HTTP/2 or gRPC over TCP inside unless measurements say otherwise.
11How to say it in the interview
When I ask "TCP or UDP?", I'm listening for three things: that you know what TCP's reliability costs (round trips and head-of-line blocking), that you can say what a UDP app has to rebuild, and that you pick per use case rather than per system.
In the interview
"TCP gives me a reliable, ordered byte stream: a one-round-trip handshake, sequence numbers and ACKs with retransmission, flow control so the receiver isn't overrun, and congestion control so the network isn't. The cost is setup latency and head-of-line blocking, where one lost packet holds up everything behind it. UDP is an 8-byte header with ports and a checksum and nothing else, so the application decides what to rebuild. For our API calls, payments and chat I'd use TCP, with pooled keep-alive connections, because a fresh HTTPS connection costs two extra round trips before the request. For the video call media I'd use UDP, because a late frame is worthless and I'd rather conceal the gap than wait for a resend. If mobile tail latency matters, I'd serve HTTP/3 at the edge: QUIC gives TCP-style reliability per stream over UDP, a combined 1-RTT handshake with TLS 1.3, and connections that survive a switch from Wi-Fi to cellular, while keeping a TCP fallback for networks that block UDP."
On the whiteboard
- TCP: handshake (1 RTT) · seq + ACK + retransmit · in-order · flow ctl (receiver) · congestion ctl (network)
- Cost: setup RTTs, slow start (IW 10 segments), head-of-line blocking
- Teardown: FIN/ACK each way · closer waits in TIME_WAIT (2 x MSL; Linux 60 s)
- UDP: 8-byte header (ports, length, checksum) · no delivery, order or state
- UDP apps rebuild: IDs, seq numbers, selective resend or skip, rate control
- QUIC = UDP + reliability + TLS 1.3 in user space · per-stream order · 1-RTT (0 on resume) · migration
- Pool connections: fresh HTTPS ≈ 3 RTT vs reused ≈ 1 RTT
- TCP: APIs, payments, chat · UDP: voice/video, game state, DNS, metrics
Further reading (6)
- RFC 9293: Transmission Control Protocol (TCP): The current TCP spec: the handshake, sequence numbers, the state machine and the 2-minute MSL behind TIME_WAIT.
- RFC 768: User Datagram Protocol: Three pages that define all of UDP: four header fields and the checksum rule.
- RFC 9000: QUIC, a UDP-based multiplexed and secure transport: Streams without cross-stream blocking, connection IDs and connection migration, straight from the spec.
- High Performance Browser Networking: Building blocks of TCP (Ilya Grigorik): Free book chapter that makes handshake cost, slow start and head-of-line blocking concrete with latency maths.
- High Performance Browser Networking: Building blocks of UDP (Ilya Grigorik): What UDP leaves out, and the NAT and firewall realities any UDP-based protocol has to live with.
- Employing QUIC protocol to optimize Uber's app performance (Uber Engineering): Production measurements of TCP loss on mobile networks and the tail-latency gain after moving to QUIC.