TCP vs UDP and the three-way handshake
40 min · Intermediate
Need help? Ask a tutor on WhatsApp (opens in a new tab)A payment screen times out on one phone while a video call freezes on another. Interviewers ask TCP vs UDP to see whether you can pick the right transport for each problem and explain the handshake and loss recovery in plain words.
Connection-oriented vs connectionless
A connection-oriented protocol sets up a connection first, then numbers, acknowledges and orders every byte. TCP is connection-oriented. UDP is connectionless: it sends each message (datagram) on its own, with no setup and no guarantees.
The first example prints TCP and UDP side by side in one table. Interviewers expect you to rebuild this table from memory.
Example 1 · TCP vs UDP side by side
rows = { "Connection": ("handshake first", "none"), "Delivery": ("guaranteed, retransmits", "best effort"), "Order": ("in order", "any order"), "Header": ("20+ bytes", "8 bytes"), "Data unit": ("byte stream", "datagram"), "Used by": ("HTTP, SSH, SMTP", "DNS, video calls, games"),}
print(f"{'':<11} {'TCP':<25} UDP")for topic, (tcp, udp) in rows.items(): print(f"{topic:<11} {tcp:<25} {udp}")TCP UDP Connection handshake first none Delivery guaranteed, retransmits best effort Order in order any order Header 20+ bytes 8 bytes Data unit byte stream datagram Used by HTTP, SSH, SMTP DNS, video calls, games
What this code does
- Every row is a trade-off: TCP adds setup, ordering and retransmission; UDP removes them and keeps a tiny 8-byte header.
- This table is the most common way interviewers check the topic: be able to fill it from memory.
Think of TCP as a phone call: you dial, the other side picks up, and you know they heard you. UDP is like dropping postcards in a box: quick and cheap, but some may arrive late, out of order or not at all.
Quick facts:
- TCP header: at least 20 bytes. UDP header: 8 bytes (ports, length, checksum).
- TCP is a byte stream; UDP keeps message boundaries.
- Both use port numbers to reach the right program, and both have a checksum.
Three-way handshake and four-way termination
The three-way handshake is how TCP opens a connection: the client sends SYN, the server answers SYN-ACK, and the client sends ACK. Both sides learn each other's starting sequence number and know that both directions work.
The second example prints the opening handshake and the four closing segments with real sequence and ack numbers, so you can see where each number comes from.
Example 2 · Handshake and termination with sequence numbers
def send(sender, receiver, flags, seq, ack=None): ack_part = "" if ack is None else f" ack={ack}" print(f"{sender:<6} -> {receiver:<6} {flags:<7} seq={seq}{ack_part}")
x, y = 1000, 5000print("Open:")send("Client", "Server", "SYN", x)send("Server", "Client", "SYN-ACK", y, x + 1)send("Client", "Server", "ACK", x + 1, y + 1)
print("Client sends 300 bytes, then close:")client_seq = x + 1 + 300send("Client", "Server", "FIN", client_seq, y + 1)send("Server", "Client", "ACK", y + 1, client_seq + 1)send("Server", "Client", "FIN", y + 1, client_seq + 1)send("Client", "Server", "ACK", client_seq + 1, y + 2)print("Client waits in TIME_WAIT, then CLOSED")Open: Client -> Server SYN seq=1000 Server -> Client SYN-ACK seq=5000 ack=1001 Client -> Server ACK seq=1001 ack=5001 Client sends 300 bytes, then close: Client -> Server FIN seq=1301 ack=5001 Server -> Client ACK seq=5001 ack=1302 Server -> Client FIN seq=5001 ack=1302 Client -> Server ACK seq=1302 ack=5002 Client waits in TIME_WAIT, then CLOSED
What this code does
- SYN and FIN each use up one sequence number, which is why the replies acknowledge x+1 and y+1 even though no data was sent. Real stacks choose random starting numbers; 1000 and 5000 keep the trace readable.
- After 300 data bytes the client's next number is 1301. The close takes four segments because each side shuts its own direction.
Closing takes four steps, because each direction is closed separately: the client sends FIN, the server ACKs it, the server later sends its own FIN, and the client ACKs that. The side that closes first waits in TIME_WAIT for a short time so late packets cannot confuse a new connection.
Order of execution
Put the handshake in order
A browser opens a connection to a web server. Put the segments in the order they are sent.
Reliability: sequence numbers, ACKs and retransmission
A cumulative ACK carries the number of the next byte the receiver expects, which confirms every byte before it. When a segment is missing, the receiver keeps repeating the same ACK number until the gap is filled.
The third example loses one segment on purpose and shows the receiver repeating the same ACK until the missing bytes are resent.
Example 3 · Retransmission when a segment is lost
SEGMENT_SIZE = 100lost_on_first_try = {3}received = set()
def cumulative_ack(): expected = 0 while expected in received: expected += SEGMENT_SIZE return expected
for i in range(1, 6): seq = (i - 1) * SEGMENT_SIZE if i in lost_on_first_try: print(f"send seq={seq} LOST") continue received.add(seq) print(f"send seq={seq} receiver ACK={cumulative_ack()}")
print("timeout or 3 duplicate ACKs for 200 -> resend seq=200")received.add(200)print(f"receiver ACK={cumulative_ack()} (all {cumulative_ack()} bytes in order)")send seq=0 receiver ACK=100 send seq=100 receiver ACK=200 send seq=200 LOST send seq=300 receiver ACK=200 send seq=400 receiver ACK=200 timeout or 3 duplicate ACKs for 200 -> resend seq=200 receiver ACK=500 (all 500 bytes in order)
What this code does
- After segment 3 (bytes 200-299) is lost, the receiver keeps answering ACK=200: it holds segments 4 and 5 but can only acknowledge what is complete. Those repeated answers are the duplicate ACKs.
- Once 200 is resent, the gap closes and one cumulative ACK=500 confirms everything. The app always sees the bytes in order.
Each byte TCP sends has a sequence number. Out-of-order data is held and put back in order before the app sees it.
If an ACK does not arrive before a retransmission timeout, the sender sends the data again. Three duplicate ACKs for the same number also signal a loss, and the sender resends early (fast retransmit) without waiting for the timer.
Flow control vs congestion control
Flow control stops a sender from overwhelming the receiver, using the receive window (rwnd). Congestion control stops senders from overwhelming the network, using the congestion window (cwnd).
The fourth example prints the congestion window round by round: it doubles in slow start, grows by one per round, then halves after a loss.
Example 4 · Congestion window: slow start and AIMD
cwnd, threshold, rwnd = 1, 16, 40for rtt in range(1, 13): loss = rtt == 8 can_send = min(cwnd, rwnd) phase = "slow start" if cwnd < threshold else "congestion avoidance" print(f"RTT {rtt:>2} cwnd={cwnd:>2} send={can_send:>2} {phase}{' LOSS' if loss else ''}") if loss: threshold = cwnd // 2 cwnd = threshold elif cwnd < threshold: cwnd *= 2 else: cwnd += 1RTT 1 cwnd= 1 send= 1 slow start RTT 2 cwnd= 2 send= 2 slow start RTT 3 cwnd= 4 send= 4 slow start RTT 4 cwnd= 8 send= 8 slow start RTT 5 cwnd=16 send=16 congestion avoidance RTT 6 cwnd=17 send=17 congestion avoidance RTT 7 cwnd=18 send=18 congestion avoidance RTT 8 cwnd=19 send=19 congestion avoidance LOSS RTT 9 cwnd= 9 send= 9 congestion avoidance RTT 10 cwnd=10 send=10 congestion avoidance RTT 11 cwnd=11 send=11 congestion avoidance RTT 12 cwnd=12 send=12 congestion avoidance
What this code does
- The window doubles 1, 2, 4, 8, 16 in slow start, then grows by one per round trip. On the loss at RTT 8 it halves, which is the "multiplicative decrease" of AIMD.
- The sender sends
min(cwnd, rwnd): cwnd is the network limit (congestion control) and rwnd is the receiver limit (flow control). This is a simplified model in segments; real TCP variants such as Reno and CUBIC differ in detail.
The sender never has more unacknowledged bytes in flight than the receive window allows.
Congestion control keeps a congestion window (cwnd) that starts small and doubles each round trip (slow start) up to a threshold, then grows by one segment per round trip. On loss it cuts the window; this pattern is AIMD (additive increase, multiplicative decrease). The sender may send min(rwnd, cwnd).
When UDP fits: video calls, DNS, games
Best-effort delivery means the network tries to deliver a datagram but promises nothing: it may be lost, duplicated or arrive out of order. UDP is best effort, so the application adds only the reliability it needs.
The last example chooses TCP or UDP for five real apps by asking one question: is late data still useful?
Example 5 · Choosing TCP or UDP for an app
from typing import NamedTuple
class Need(NamedTuple): every_byte_matters: bool late_data_useless: bool
def choose(need): return "UDP" if need.late_data_useless and not need.every_byte_matters else "TCP"
apps = { "UPI payment": Need(True, False), "Video call": Need(False, True), "Download results PDF": Need(True, False), "Multiplayer game moves": Need(False, True), "Email (SMTP)": Need(True, False),}for app, need in apps.items(): print(f"{app:<23} {choose(need)}")UPI payment TCP Video call UDP Download results PDF TCP Multiplayer game moves UDP Email (SMTP) TCP
What this code does
- The question to ask is: is late data still useful? For a payment or a file it is, so TCP. For a voice packet that is 500 ms late, it is not, so UDP.
- Real apps mix both: a game may send moves over UDP and chat or purchases over TCP.
Example 6 · TCP and UDP sockets on your own computer
import socketimport threading
def echo_once(server): conn, _ = server.accept() with conn: conn.sendall(conn.recv(1024).upper())
# TCP: connect first, then exchange a byte stream (all on this computer, no internet needed)with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as server: server.bind(("127.0.0.1", 0)) # port 0 lets the OS pick a free port server.listen() worker = threading.Thread(target=echo_once, args=(server,)) worker.start() with socket.create_connection(server.getsockname()) as client: client.sendall(b"hello tcp") print("TCP reply:", client.recv(1024).decode()) worker.join()
# UDP: no connection, just send one datagram to an addresswith socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as receiver, \ socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sender: receiver.bind(("127.0.0.1", 0)) sender.sendto(b"hello udp", receiver.getsockname()) data, _ = receiver.recvfrom(1024) print("UDP got:", data.decode())
print("TCP is", socket.SOCK_STREAM.name, "and UDP is", socket.SOCK_DGRAM.name)TCP reply: HELLO TCP UDP got: hello udp TCP is SOCK_STREAM and UDP is SOCK_DGRAM
What this code does
- A TCP socket connects first and then carries a byte stream; the server here replies in capitals.
- A UDP socket just sends one datagram to an address, with no connection and no reply.
- Port 0 asks the operating system for a free port, and 127.0.0.1 keeps everything on this computer, so no internet is needed.
In a video call, a frame that arrives late is useless, so resending it only adds delay. DNS uses UDP because one small question and one answer need no connection setup. Choose TCP when every byte must arrive correctly: web pages, UPI payments, file downloads, email and database connections.
Common choices:
- UDP: video and voice calls, live streaming, online games, DNS queries, DHCP.
- TCP: HTTP/1.1 and HTTP/2, SSH, SMTP, FTP, MySQL connections.
- QUIC (used by HTTP/3) runs on UDP and adds its own reliability and encryption.
Find the bug
Spot the wrong statement
Each note has one wrong line. Tap it, then check the fix.
A candidate describes UDP.
Round 1 of 3Next step
Check your understanding
12 quick questions