System Internals
Open the simulator β†’
Absolute basics

TCP vs UDP

The two ways to send data across a network: a reliable conversation, or a quick shout.

DNS turned a name into an IP, and client↔server showed one request crossing the network and coming back. But what actually carries those bytes across the wire? Almost always one of two transport protocols: TCP or UDP. TCP is a careful phone call β€” it sets up a connection, numbers every byte, and resends anything that gets lost so the data arrives complete and in order. UDP is a postcard β€” it just throws a packet at the destination with no setup and no guarantees. Picking between them is one of the most fundamental trade-offs in networking: reliability versus speed.

The big picture#

TL;DRthe 30-second version
  • TCP and UDP both ride on top of IP (which only knows how to shove a single packet toward an address, with no promise it arrives). They are the two common ways to use that raw delivery.
  • TCP is reliable, ordered, and connection-oriented: it does a 3-way handshake (SYN, SYN-ACK, ACK), numbers every byte, acknowledges what arrives, and retransmits what's lost β€” so the receiver gets a complete, in-order byte stream. It also adds flow control and congestion control.
  • UDP is connectionless and unreliable: no handshake, no acknowledgements, no retransmission, no ordering. It just sends independent datagrams with an 8-byte header and hopes they arrive. That makes it lean and fast.
  • The rule of thumb: use TCP when every byte must arrive correctly (web pages, files, APIs, email); use UDP when fresh-and-fast beats complete-and-late (live video/voice, online games, DNS). Modern HTTP/3 runs QUIC over UDP to get TCP's reliability without TCP's head-of-line blocking.

Everything below expands on these points. Read the core sections top to bottom for the full mental model; the collapsible "Go deeper" boxes hold the advanced internals (sequence numbers, the sliding window, congestion control, Nagle, head-of-line blocking, QUIC) you can skip on a first pass and come back to later.

Clientwants to sendServerlistening
SYN β€” β€œlet’s talk; my seq starts at x”
SYN-ACK β€” β€œok, my seq is y; got your x”
ACK β€” β€œgreat, got your y”
connection established
data, data, data…
ACKs β€” each chunk acknowledged
TCP's 3-way handshake: agreeing to talk before sending data

Start here: the network underneath drops things#

Underneath both protocols sits IP, the Internet Protocol. IP's whole job is to take one packet and route it toward an address β€” and that's all it promises. It does not promise the packet will arrive. It does not promise packets arrive in the order you sent them. It does not even promise a packet arrives only once. Routers get congested and drop packets; different packets can take different paths and show up out of order; a retransmitting link can deliver a duplicate. IP is 'best effort', which is a polite way of saying 'no guarantees'.

That raw delivery is too unreliable to build most software on directly. If you're downloading a file, a single dropped packet means a corrupted file. If you're loading a web page, bytes arriving out of order would be gibberish. So someone has to add the missing guarantees β€” detect loss, resend, put things back in order β€” on top of IP. That 'someone' is the transport layer, and TCP is the protocol that adds all of it.

But guarantees aren't freeAdding reliability costs time: a connection setup before you can send, waiting for acknowledgements, and pausing to resend lost data. For some applications that cost is unacceptable β€” a live voice call would rather skip a lost 20-millisecond audio chunk than freeze the whole call waiting for it to be resent. For those, UDP deliberately keeps IP's bare-bones model and adds almost nothing. So we end up with two protocols: TCP for 'I need all of it, correct and in order', and UDP for 'I need it now, and stale data is useless'.

How TCP makes it reliable (and how UDP doesn't bother)#

TCP turns IP's unreliable packet delivery into a reliable, ordered byte stream. To the application it looks like a clean pipe: you write bytes in one end, the exact same bytes come out the other end, in the same order, with nothing missing or duplicated. Underneath, TCP earns that illusion with four mechanisms working together.

  1. Connection setup (the 3-way handshake): before any data, the two sides exchange SYN β†’ SYN-ACK β†’ ACK. This confirms both can send and receive, and lets each side announce its starting sequence number. Only after this is the connection 'established'.
  2. Sequence numbers + ordering: TCP numbers every byte it sends. The receiver uses those numbers to reassemble the stream in the right order, even if packets arrive scrambled, and to spot duplicates and drop them.
  3. Acknowledgements + retransmission: the receiver sends back ACKs saying 'I have everything up through byte N'. If the sender doesn't get an ACK in time (a timeout) or sees signs of loss, it resends the missing data. This is how loss is repaired.
  4. Flow control + congestion control: TCP also paces itself. Flow control stops a fast sender from overwhelming a slow receiver's buffer; congestion control stops senders from overwhelming the network itself. Together they keep the connection from drowning either endpoint or the path between them.

UDP does essentially none of this. There is no handshake β€” the first packet is data. There are no sequence numbers exposed, no acknowledgements, no retransmission, no reordering, no flow or congestion control. UDP adds just two things to a raw IP packet: port numbers (so the data reaches the right application on the machine) and a checksum (so a corrupted datagram can be detected and discarded). Each datagram is independent β€” sent, and forgotten. If it's lost, UDP neither knows nor cares; it's the application's problem if it matters at all.

SenderTCPReceiverTCP
seq 1
ack 1
seq 2
seq 3
seq 3 arrives but can’t be delivered yet β€” gap at 2
timeout / dup-acks β†’ retransmit
resend seq 2
ack 3 β€” gap filled, deliver 1,2,3 in order
A lost segment: TCP detects and retransmits (UDP just loses it)
TCP is a byte stream; UDP is messagesOne more difference that trips people up: TCP has no notion of 'messages' β€” it's a continuous stream of bytes, so two sends of 100 bytes might arrive as one read of 200 or two reads of 50+150. The application must frame its own message boundaries. UDP preserves boundaries: one sendto of a datagram becomes exactly one recvfrom of that datagram (or nothing, if it's lost). So UDP keeps your message edges; TCP smears them into a stream.
PredictAn app sends three UDP datagrams in order: A, B, C. The network delays B so it takes a longer path. What does the receiver get, and in what order?

Hint: UDP doesn't reorder, doesn't wait, and doesn't resend. What's left?

Most likely A, then C, then B β€” out of order β€” because UDP delivers each datagram the moment it arrives and never buffers to wait for a straggler. And if B had been dropped instead of merely delayed, the receiver would simply get A then C with no indication that anything is missing. With TCP, the same scenario would deliver A, B, C in order: it would hold C until B arrived (or was retransmitted) before handing the stream up. That holding-and-waiting is exactly the ordering guarantee β€” and, as we'll see, the source of head-of-line blocking.

The cost model: setup round-trips, header bytes, latency#

There's no Big-O here β€” the costs are measured in round-trips, bytes of overhead, and added latency. Three numbers capture most of the difference between TCP and UDP.

  • Setup cost: TCP pays a full round-trip for the handshake before any data flows (SYN/SYN-ACK/ACK is 1.5 round-trips, but the sender can send data with its final ACK, so the data is delayed by ~1 RTT). Over TLS on top, that's another 1–2 round-trips. UDP pays zero setup β€” the first packet is already your data.
  • Header overhead: a UDP header is just 8 bytes (source port, destination port, length, checksum). A TCP header is 20 bytes minimum (and up to 60 with options), because it must carry sequence and acknowledgement numbers, window size, flags, and more. On tiny messages that difference matters; on a big download it's negligible.
  • Latency under loss: this is the big one. With TCP, a single lost packet stalls delivery of everything after it until the retransmission arrives β€” adding a round-trip (or more) of delay to that whole stream. With UDP, loss adds zero latency: the next datagram is delivered immediately; the lost one is simply absent.
Why 'reliable' can mean 'slower when it matters most'TCP's guarantee is in-order delivery, and the only way to deliver in order across a lossy network is to wait for the missing piece before releasing what came after it. So exactly when the network is dropping packets β€” the worst moment β€” TCP introduces stalls. That's perfect for a file (you want every byte, a little late is fine) and terrible for a live call (a stall is a freeze; the late audio is worthless by the time it arrives). The cost model is why the use case decides the protocol.
PredictYou fire off a one-shot request that fits in a single packet to a server 50 ms away and need the reply ASAP. Over a fresh TCP connection vs. over UDP, roughly how much sooner does UDP let you send that request?

Hint: Count the handshake round-trips TCP must complete first.

About one round-trip (~50 ms) sooner. TCP must complete its handshake before your data is delivered, so you wait ~50 ms for the SYN/SYN-ACK before the request even goes out. UDP sends the request in the very first packet β€” no setup. This 'save a round-trip on small one-shot exchanges' is a big reason DNS queries use UDP, and a big reason QUIC (over UDP) works so hard to make connection setup 0-RTT or 1-RTT.

Inside the machinery: windows, congestion control, and QUIC#

The four mechanisms above are enough for a working mental model. The deep dives below open up the parts that come up in real systems and interviews: how TCP sends fast without waiting for every ACK, how it shares the network politely, two famous performance gotchas, and how QUIC re-imagines all of it on top of UDP.

Go deeperGo deeper: sequence numbers and the sliding window

If TCP sent one segment and waited for its ACK before sending the next, throughput would be terrible β€” you'd send at most one segment per round-trip. Instead TCP uses a sliding window: it can have many bytes 'in flight' (sent but not yet acknowledged) at once. The receiver advertises a receive window β€” how much buffer space it has free β€” and the sender keeps that much unacknowledged data on the wire, sliding the window forward as ACKs come back. This is also TCP's flow control: if the receiver's application is slow to read, its advertised window shrinks (down to zero if its buffer fills), and the sender automatically throttles so it never overruns a slow receiver.

Sequence numbers make this work. Every byte has a number, ACKs are cumulative ('I have everything up to byte N'), and the receiver buffers out-of-order segments until the gap is filled. Modern TCP adds selective acknowledgement (SACK) so the receiver can say 'I have 1–1000 and 1500–2000 but I'm missing 1000–1500', letting the sender resend only the actual hole instead of everything after it.

Go deeperGo deeper: congestion control and AIMD

Flow control protects the receiver; congestion control protects the network. There's no central authority telling each TCP sender how fast to go, yet millions of connections share the same links without permanently collapsing. They cooperate through a feedback rule. TCP keeps a congestion window (cwnd) that limits how much it sends; the amount in flight is the smaller of the receive window and the congestion window.

Classic TCP follows AIMD β€” Additive Increase, Multiplicative Decrease. While packets are getting through, it grows the window slowly and linearly (additive increase: +1 segment per round-trip). The moment it detects loss β€” a signal of congestion β€” it cuts the window sharply, roughly in half (multiplicative decrease). This gentle-up, sharp-down rhythm is what produces TCP's characteristic sawtooth throughput, and AIMD is mathematically the thing that lets independent senders converge toward sharing a link fairly. Before AIMD even kicks in, a new connection uses slow start: it begins with a tiny window and doubles it each round-trip until it hits a threshold or sees loss, quickly probing how much the path can take.

Go deeperGo deeper: Nagle's algorithm and delayed ACKs

Sending lots of tiny packets is wasteful β€” a 1-byte payload still drags a 20-byte TCP header (plus 20 for IP) behind it. Nagle's algorithm fights this by buffering small writes: it holds a small outgoing chunk until either it has a full segment's worth of data or all previously sent data has been acknowledged. It coalesces many tiny writes into fewer full packets.

The trouble is Nagle can interact badly with delayed ACKs (where the receiver waits a few milliseconds before acking, hoping to piggyback the ACK on a reply). You can get a standoff: the sender is waiting for an ACK before sending its small next chunk, and the receiver is delaying that ACK β€” adding tens of milliseconds of latency to small request/response exchanges. This is why latency-sensitive applications often set the TCP_NODELAY socket option to disable Nagle. It's a classic real-world TCP gotcha worth being able to name.

Go deeperGo deeper: head-of-line blocking and why QUIC runs over UDP

TCP's in-order guarantee has a sharp edge called head-of-line (HoL) blocking. Because TCP delivers a single ordered stream, one lost segment blocks delivery of every byte after it β€” even bytes that have already arrived safely β€” until the loss is repaired. That's fine for one file. But HTTP/2 multiplexes many independent requests over one TCP connection; if a single packet is lost, all of those logically-independent streams stall together, because TCP can't tell that the bytes belong to different requests. The reliability layer is too coarse.

QUIC (RFC 9000), the transport beneath HTTP/3, fixes this by rebuilding reliability on top of UDP. QUIC takes UDP's bare datagrams and re-adds the good parts of TCP β€” connection setup, acknowledgements, retransmission, flow and congestion control β€” but with independent streams as a first-class concept. A lost packet only stalls the specific stream whose data it carried; the others keep flowing. QUIC also folds the TLS handshake into the transport handshake (often 1-RTT, or 0-RTT for resumed connections), encrypts almost everything including headers, and can survive a change of network (a connection ID outlives a switch from Wi-Fi to cellular). It's the clearest illustration of the whole topic: UDP is the minimal base, and you layer exactly the guarantees you want on top β€” TCP is one such layering, QUIC is a newer one.

What each one buys, and what it costs#

TCP and UDP sit at opposite ends of the same dial: reliability and order on one end, minimal latency and overhead on the other. Neither is 'better' β€” they're tuned for different needs.

  • TCP strength β€” correctness: a complete, in-order, de-duplicated byte stream with no work from the application. Loss, reordering, and duplication are all handled for you, and it won't overwhelm the receiver or the network.
  • TCP cost β€” latency and rigidity: a setup round-trip before data, and head-of-line blocking means one lost packet can stall everything behind it. Great for throughput-oriented, must-arrive data; poor for real-time streams that prefer fresh-over-complete.
  • UDP strength β€” speed and control: no handshake, tiny 8-byte header, no stalls on loss, and it preserves message boundaries. The application gets the raw timing of the network and can choose exactly which guarantees (if any) it wants to add.
  • UDP cost β€” you're on your own: no reliability, ordering, flow control, or congestion control. If you need any of those, you must build them yourself β€” and a UDP app that ignores congestion control can be a bad citizen that floods a network.
The deciding questionAsk: 'is a late-but-correct byte worth more than an on-time one?' If yes (files, web pages, payments, email), you want TCP β€” every byte matters and a little delay is fine. If no (live video, voice, gaming, telemetry, DNS), you want UDP β€” by the time TCP finished resending that lost audio frame, the moment it described is already gone, so you'd rather skip it and stay live. That single question resolves most TCP-vs-UDP decisions.

How each one fails#

  • TCP β€” head-of-line blocking: one lost packet stalls delivery of all subsequent data on that connection until it's retransmitted. On a multiplexed connection (HTTP/2), independent requests stall together. QUIC/HTTP-3 over UDP is the modern answer.
  • TCP β€” half-open and stuck connections: if one side vanishes without a proper close (crash, cable pulled), the other can hold a 'connection' that no longer exists. Keepalives and timeouts are needed to notice and reclaim it.
  • UDP β€” silent loss and reordering: datagrams can vanish or arrive out of order with no notification at all. Any application that cares must add its own sequence numbers, acknowledgements, or forward-error-correction.
  • UDP β€” amplification and floods: because there's no handshake to prove the sender's address, attackers spoof a victim's IP and trick UDP services (DNS, NTP) into blasting large replies at the victim β€” a reflection/amplification DDoS. The same no-handshake property that makes UDP fast makes it easy to abuse.
Loss is normal, not exceptionalIt's tempting to think of packet loss as a rare error. On the real internet it's a routine, expected event β€” congested routers drop packets as their normal way of signaling 'slow down', and Wi-Fi/cellular links lose packets constantly. That's why TCP's loss recovery isn't an edge case but the core of its design, and why a UDP application that needs any reliability must plan for loss from the start rather than treat it as an afterthought.

TCP vs UDP at a glance#

TCPUDP
ConnectionConnection-oriented (3-way handshake first)Connectionless (just send)
ReliabilityReliable β€” ACKs + retransmissionUnreliable β€” no ACKs, no resend
OrderingOrdered byte streamUnordered, independent datagrams
BoundariesStream (no message boundaries)Preserves message boundaries
Flow / congestion controlYes (sliding window + AIMD)No (app's responsibility)
Header size20 bytes min (up to 60)8 bytes, fixed
Setup latency~1 RTT before data (more with TLS)Zero β€” first packet is data
Latency under lossStalls (head-of-line blocking)None β€” lost packet just skipped
Use casesWeb/HTTP, files, email, SSH, databasesDNS, video/voice, games, QUIC/HTTP-3

Read the table as one consistent story: every TCP 'yes' (reliable, ordered, controlled) costs latency and header bytes, and every UDP 'no' buys speed and leanness at the price of guarantees you must supply yourself if you need them.

Where each one shows up#

  • TCP runs the workhorse web: HTTP/1.1 and HTTP/2, all file transfers, email (SMTP/IMAP/POP), SSH, and most database wire protocols β€” anywhere a missing or reordered byte would corrupt the result.
  • UDP runs DNS: a lookup is a tiny query and a tiny answer, and skipping a handshake saves a whole round-trip on something that happens before every connection. If a UDP query is lost, the resolver just asks again.
  • UDP runs real-time media: Zoom, WebRTC video/voice calls, and live streaming protocols send audio/video over UDP (often via RTP) because a frozen-but-complete call is worse than a call that drops the occasional frame and stays live.
  • UDP runs online games: fast-paced multiplayer sends frequent position updates over UDP; a stale update is useless, so resending it would only add lag β€” better to ignore the loss and trust the next update.
  • QUIC/HTTP-3 runs over UDP: Google, Cloudflare, and others now serve a large share of web traffic over QUIC, getting TCP-like reliability with per-stream independence and faster setup β€” the modern proof that 'TCP vs UDP' isn't the end of the story, it's the foundation you build new transports on.
DNS straddles bothDNS is the perfect case study: it defaults to UDP for speed on small queries, but falls back to TCP when the answer is too big to fit in a single datagram (large responses, zone transfers, and DNSSEC-signed records). One protocol pair, chosen per message by size β€” exactly the trade-off this topic is about.

Common questions & gotchas#

Is TCP always the 'safe default'?

For data that must arrive complete and in order, yes β€” and that covers most applications, which is why it's the default for HTTP, files, and APIs. But 'safe' has a cost: setup latency and head-of-line blocking. For real-time media, games, and DNS, UDP is the correct default precisely because TCP's reliability would hurt more than help. The right default depends on whether late-but-correct beats on-time.

Is UDP just 'unreliable TCP'?

No β€” it's the bare transport layer with almost nothing added (just ports and a checksum on top of IP). TCP is the one that adds a lot. The useful framing is the other way around: UDP is the minimal base, and TCP is one particular set of guarantees layered on top of that base. QUIC is a different, newer set of guarantees layered on the same base.

Can you get reliability over UDP?

Yes β€” you just have to build it. The application (or a library) adds its own sequence numbers, acknowledgements, and retransmission for the parts that need it, while leaving the rest fast. QUIC is exactly this done well: full reliability and congestion control over UDP, but with independent streams so it avoids TCP's head-of-line blocking. Game engines and media protocols often add partial reliability (resend critical events, drop stale position updates).

Why does a one-byte TCP message still feel heavy?

Because TCP can't send a lone byte cheaply: it carries a 20-byte header, may wait on Nagle's algorithm to batch small writes, and the receiver may delay the ACK β€” so a trickle of tiny messages can pile up latency and overhead. Latency-sensitive apps disable Nagle (TCP_NODELAY) or batch their own writes. UDP avoids this by simply sending the datagram immediately.

QuizA multiplayer game sends 60 position updates per second over the network. One update packet is lost. What's the best behavior, and which protocol fits it?

  1. Stall the game and retransmit the lost update before showing anything newer β€” use TCP
  2. Ignore the lost update and use the next one that arrives ~16 ms later β€” use UDP
  3. Resend every update twice over TCP to be safe
  4. Switch to TCP only when a packet is lost
Show answer

Ignore the lost update and use the next one that arrives ~16 ms later β€” use UDP β€” A position from 16 ms ago is already stale β€” by the time TCP retransmitted it, two or three newer positions would have arrived, so waiting for it only adds lag and shows the player an out-of-date world. The right move is to skip the lost update and act on the freshest one. That 'fresh beats complete' need is exactly what UDP is for; TCP's insistence on in-order delivery would actively hurt here.

In an interview#

Lead with the contrast in one breath: TCP is connection-oriented, reliable, and ordered β€” a 3-way handshake (SYN/SYN-ACK/ACK), then a byte stream with sequence numbers, acknowledgements, retransmission, flow control, and congestion control. UDP is connectionless and unreliable β€” no handshake, no ACKs, no ordering, just independent 8-byte-header datagrams. TCP guarantees delivery; UDP guarantees nothing but speed.

Then show you can choose between them by the use case: TCP for web/files/email/databases where every byte must arrive; UDP for DNS, live video/voice, and games where fresh-and-fast beats complete-and-late. Name the cost model β€” TCP's setup round-trip and head-of-line blocking versus UDP's zero setup and zero stall on loss β€” because that's the reasoning behind the choice, not just the labels.

Close by going one level deeper to stand out: mention the sliding window and AIMD congestion control as how TCP is fast yet fair, name head-of-line blocking as TCP's key weakness, and explain that QUIC/HTTP-3 runs over UDP to rebuild reliability with independent streams and faster handshakes. If pressed on a gotcha, cite Nagle's algorithm interacting with delayed ACKs and the TCP_NODELAY fix. Then open the simulator and watch a handshake, a lost segment getting retransmitted, and a UDP datagram sail through with no recovery.

References & further reading#

References