Skip to main content
← Back to Insights

TCP vs UDP - Under the Hood

· 18 min read
Jitender Sharma
Advisor & Technical Leader · Enterprise AI & Platforms

Side-by-side TCP reliable stream with handshake and ACKs versus UDP datagrams with no delivery guarantee

HTTPS feels solid. Live video and classic DNS often gamble on packets that may never arrive. Same Internet. Different transport contracts.

Most engineers know the slogan: TCP is reliable; UDP is fast. That is directionally right. It skips the components, the steps, and the use cases that decide which failures belong to the network versus your app.

TCP gives you an ordered byte stream with retransmission and congestion control. UDP gives you datagrams and almost nothing else. Pick wrong and you either pay latency you did not need, or you reinvent reliability badly.

THE CLAIM

TCP buys reliability and order with state, handshake cost, and head-of-line blocking. UDP buys simplicity and low overhead; delivery, order, and congestion are your problem (or QUIC’s). Transport choice is an architecture contract, not a default checkbox.

The bottom line first

  • TCP: connection-oriented byte stream; handshake, sequence numbers, ACKs, retransmission, congestion control.
  • UDP: connectionless datagrams; send and (maybe) receive; no built-in delivery guarantee.
  • Both use ports on top of IP so many apps can share one host.
  • Reliability is not free: TCP pays RTT and buffers; UDP pushes loss handling into the application or a layer like QUIC.
  • Classic use map: web, SSH, databases → TCP; DNS queries, NTP, media, many games, HTTP/3 (QUIC) → UDP.
  • Observability differs: TCP shows RSTs, retransmits, window collapse; UDP shows silent drops and app-level timeouts.

What TCP and UDP actually are

Both sit in the transport layer (Layer 4) above IP. IP (Layer 3) delivers packets between hosts. TCP and UDP (Layer 4) deliver data between applications (via ports). Applications such as HTTP and DNS sit above that (Layer 7 in the OSI map people usually mean).


TCPUDP
AbstractionReliable, ordered byte streamDiscrete datagrams
ConnectionYes (state on both ends)No (stateless sends)
DeliveryRetransmit until ACK (or give up)Best effort; loss is silent to the stack
OrderIn-order delivery to the appArrival order not guaranteed
OverheadHigher (headers + state + ACKs)Lower
CongestionBuilt-in controlApplication / upper layer

Components involved

ComponentWhat it isTCPUDP
SocketApp endpoint (IP + port + protocol)Connected after handshakeBound; send/recv datagrams
Port16-bit app demux (443, 53, …)YesYes
Segment / datagramTransport PDU inside an IP packetTCP segmentUDP datagram
Sequence / ACKTrack bytes and confirm deliveryCoreAbsent
ChecksumDetect corruptionYesYes (often required in practice)
Window / bufferFlow and congestion controlYesApp manages pacing
Firewall / NAT stateMiddlebox trackingConntrack feels “natural”Short-lived or UDP timeout quirks
PORTS ARE NOT THE PROTOCOL

Port 53 is often DNS over UDP, and sometimes DNS over TCP (large answers, zone transfer). The number names the service habit; the transport still matters.

How a flow works (steps)

TCP is a lifecycle (open → transfer → close). UDP is a send path (optional bind → datagram → hope). Below, each phase is its own section: first what is in the Layer 4 header, then what happens on the wire.

What is a TCP header, and what sits in it

A TCP header is the Layer 4 control block on every TCP segment. It tells the peer which sockets are talking, where this data sits in the byte stream, what the segment means (SYN, ACK, FIN, …), and how much more the receiver can accept. It is not the application message (HTTP, TLS, …). Those bytes, when present, follow in the payload.

So: a TCP segment = TCP header + optional payload. The header is not sent once at connect time. Every segment on the wire carries a TCP header (handshake, data, pure ACK, FIN, RST). Only the payload is sometimes empty.

What sits in that header is the field set below (ports, seq/ack, flags, window, checksum, options).

Header fields (layout)

Think of the header as a fixed control block glued in front of every chunk of (optional) data. On the wire it is laid out in 32-bit rows like this:

TCP header layout showing source and destination ports, sequence and acknowledgment numbers, flags, window, checksum, options, and optional data


Read it top to bottom: ports (which sockets), then seq / ack (reliability), then flags + window (what this segment means and flow control), then checksum, then optional options and data.

FieldBitsRoleExampleWhen it matters
Source port16Sending socket on the local host. Client usually uses an ephemeral port the OS picks.Browser open: 53122. Reply from server uses 53122 as dest port.Demux on the client; NAT maps this port; two tabs = two source ports.
Destination port16Receiving socket on the peer. Servers listen on a well-known port.HTTPS request: dest 443. SSH: dest 22. Postgres: dest 5432.Client must target the right service; firewalls filter on this.
Sequence #32Byte offset of this segment's payload in the sender's stream (after the initial SYN seq).Data segment: seq=1000, length 100 → covers bytes 1000-1099. Next data often seq=1100.Ordering, reassembly, detecting loss/duplicates.
Acknowledgment #32Next byte the sender of this segment expects from the peer. Valid when ACK flag is set.Got bytes through 1099 → send ack=1100 ("send me from 1100 onward").Cumulative ACK; gaps → dup ACKs → fast retransmit.
Flags6+ (classic)Bit switches for what this segment means. Common: SYN, ACK, FIN, RST, PSH, URG.SYN only = start connect. SYN+ACK = accept. ACK = confirming data/handshake. FIN = done sending. RST = abort.Handshake, teardown, resets, "push to app now" (PSH).
Window16Flow control: how many more bytes the receiver will accept from the peer (scaled if window-scale option negotiated).Window 65535 = "you may have up to that much unacked data toward me." Window 0 = "stop; my buffer is full."Zero-window stalls; slow consumer backs up the sender.
Checksum16Corruption check over header + payload (+ IP pseudo-header). Bad checksum → segment dropped.Bit flip in flight → receiver discards; TCP retransmits later. You rarely "read" the value in apps.Silent data corruption protection; bad NIC/path shows as loss.
Options0-320Variable extras after the base header. Common: MSS, window scale, SACK, timestamps.On SYN: MSS 1460 ("max payload I want"). Later: SACK blocks listing which holes were received.Tuning throughput, selective retransmit, RTT measurement.

TCP call start to finish: headers and payloads

Full life of one TCP connection: open → exchange application data → close. Scenario uses plain HTTP on port 80 so payloads are readable. HTTPS is the same TCP header story; the early payloads are TLS binary (ClientHello, …) instead of GET /….

ClientServer
Host203.0.113.10198.51.100.20
Port53122 (ephemeral)80 (HTTP)
Teaching ISNclient starts at seq 1000server starts at seq 5000

Real ISNs are large and random. Lengths below are small for teaching.


A. Open the call (handshake, no app payload)

1. Client → Server: SYN

Portssrc 53122 → dst 80
Seq / ackseq=1000 (ISN). Ack unused (ACK flag off)
FlagsSYN
Optionse.g. MSS 1460
Payload(empty)

2. Server → Client: SYN-ACK

Portssrc 80 → dst 53122 (swapped)
Seq / ackseq=5000 (server ISN), ack=1001 ("got your SYN")
FlagsSYN + ACK
Payload(empty)

3. Client → Server: final handshake ACK

Portssrc 53122 → dst 80
Seq / ackseq=1001, ack=5001
FlagsACK
Payload(empty)

Connection is ESTABLISHED. Next client data byte is at seq 1001; next server data byte at seq 5001.

B. Application data on the call (header + payload)

4. Client → Server: HTTP request bytes

Ports5312280
Seq / ackseq=1001, ack=5001, len=45 (covers 1001-1045)
FlagsACK + PSH
Payload (example)see below
Example request payload (bytes inside TCP)
GET / HTTP/1.1
Host: example.com
Connection: close

TCP does not understand HTTP. It only sees 45 bytes of payload. Next client seq after this segment = 1046.

5. Server → Client: pure ACK (optional alone; often delayed or piggybacked)

Ports8053122
Seq / ackack=1046 ("I have client bytes through 1045")
FlagsACK
Payload(empty). Confirms delivery only, not "HTTP 200"

6. Server → Client: HTTP response bytes

Ports8053122
Seq / ackseq=5001, ack=1046, len=70 (covers 5001-5070)
FlagsACK + PSH
Payload (example)see below
Example response payload (bytes inside TCP)
HTTP/1.1 200 OK
Content-Length: 13
Content-Type: text/plain

Hello, world!

Next server seq = 5071.

7. Client → Server: ACK for the response

Ports5312280
Seq / ackack=5071
FlagsACK
Payload(empty)
C. End the call (teardown, no app payload)

Both sides still use the same ports. FIN consumes one sequence number (like a one-byte "I'm done") even with empty payload.

8. Client → Server: FIN

Ports5312280
Seq / ackseq=1046, ack=5071
FlagsFIN + ACK
Payload(empty)

9. Server → Client: ACK of FIN, then server FIN

ACK of client FINack=1047, flags ACK, payload empty
Server FINseq=5071, ack=1047, flags FIN+ACK, payload empty

10. Client → Server: final ACK

Ports5312280
Seq / ackack=5072
FlagsACK
Payload(empty)

Client enters TIME_WAIT, then the 4-tuple is free. A new call needs a new SYN (often a new ephemeral port).

Header vs payload cheat sheet
PhaseHeader always?Typical payload
SYN / SYN-ACK / handshake ACKYesEmpty
App request / responseYesHTTP text, TLS records, DB protocol, …
Pure ACKYesEmpty
FIN / RSTYesEmpty (RST aborts; no orderly FIN dance)

HTTPS note: TCP phases are identical. After ESTABLISHED, early payloads are TLS (ClientHello, cert, …), then TLS Application Data wrapping HTTP. Wireshark: TCP flags/seq/ack for lifecycle; TLS/HTTP dissectors for payload. Companions: HTTP vs HTTPS Under the Hood for cleartext vs TLS wrap; HTTPS Encryption Lifecycle Under the Hood for one full call by phase.

Scale note: large bodies split into many segments (≤ MSS); ACKs are frequent; TCP is full duplex. The diagram is the smallest story so headers stay readable.

Reuse: keep-alive skips teardown and sends another data phase on the same ports; a new call needs a new SYN (often a new ephemeral port).

Ports: which apps (sockets) on each host

IP names hosts. Ports pick the socket on that host. Demux uses the 4-tuple (plus protocol = TCP):

source IP · source port · dest IP · dest port

SideTypical portWho chooses it
ServerWell-known (e.g. 443, 22, 5432)Service config
ClientEphemeral (e.g. 49152-65535)OS

Example: browser 53122 → server 443; reply swaps ports (44353122). Two tabs to the same site get different ephemeral ports = two connections.


HEADER ≠ HANDSHAKE

SYN is a flag in the header, not a separate header type. Every later data/ACK/FIN segment still carries a full TCP header. A port names a bound socket, not a permanent process.

TCP phase notes (beyond the walkthrough)

TopicWhat to remember
Handshake costAbout one RTT before app bytes (TLS adds more after ESTABLISHED)
LossRetransmit on timeout or duplicate ACKs (fast retransmit)
Flow / congestionReceiver window caps in-flight bytes; sender backs off on loss/delay
Head-of-lineA gap can block later bytes from the app even if they arrived
TeardownOrderly FIN/ACK each way; RST aborts. Client often sits in TIME_WAIT briefly so delayed segments cannot corrupt a new 4-tuple
TAKEAWAY

TCP wire story: header fields drive establish → reliable transfer → close. Incidents are handshake timeout, retransmits, zero window, RST, or TIME_WAIT pressure.

What sits in the UDP header

A UDP datagram = UDP header + payload. Same rule as TCP: the header is on every datagram, not only the first message. There is no separate connect header.

WhenWhat is sent
Every sendFull UDP header + that message's payload
Every replyIts own full UDP header + reply payload
FieldRoleSent when?
Source portReply address for the peer (0 if not used)Every datagram
Dest portWhich listening socket on the target hostEvery datagram
LengthHeader + payload sizeEvery datagram
ChecksumDetect corruption (header + payload + IP pseudo-header)Every datagram

Same header, every datagram (phases)

UDP has no handshake or teardown. The header is still on every datagram. Map the story as three app-level phases:


PhaseHeader sent?Typical payload
1 Bind and sendYes, every datagramRequest bytes (DNS query, game packet, …)
2 On the pathSame headers ride in IP packetsUnchanged; may never arrive
3 Reply / retryYes, every reply and retryReply bytes, or a fresh copy of the request
NO SYN / FIN

UDP never sends a connect or close header. “Session” and retries are application logic. Wire constant: datagram = UDP header + payload, every time.

TAKEAWAY

UDP wire story: tiny header plus payload, then best effort. Reliability is Layer 7 (or QUIC), not a Layer 4 feature.

Reuse and pooling

Handshake and teardown are expensive relative to a small request. Systems reuse live sessions and often pool several warm ones. TCP and UDP differ here.

TCP: keep-alive, reuse, connection pools

A TCP connection stays usable from ESTABLISHED until FIN/RST. Reuse is not another SYN on the same socket. After open, segments use ACK (often with data).

“A new SYN = a new call” means: sending SYN again starts a new TCP connection, not a continuation of the one you already have. Keep-alive and pools keep talking on the live ESTABLISHED socket with ACK + data. A fresh SYN (often with a new ephemeral port) is a brand-new handshake and a new connection.

PatternWhat it isExamples
Keep-aliveOne TCP session, many requestsHTTP/1.1 keep-alive, gRPC/HTTP/2, DB sessions
MultiplexingMany streams on one connectionHTTP/2 (HTTP/3 is QUIC on UDP)
Connection poolLibrary holds N warm socketsJDBC, HTTP clients, Redis, LB upstreams

Buys: skip repeated SYN (and often full TLS) while the socket lives. Gotchas: idle LB/NAT timeouts, server restart → ECONNRESET, stale peer after DNS change, oversized pools waste FDs. Closed TCP is not reused.

UDP: no connection to pool (usually)

UDP has no Layer 4 connection. Reuse the bound socket (many datagrams), or an app/QUIC session on top.

PatternRealityExamples
Bound socket reuseOne local UDP socket, many sendsDNS stub, NTP, games
App sessionSoft state in your codeMedia, custom RPC
QUICReal connections on UDPHTTP/3 pools

connect() on UDP only sets a default peer filter. It does not create TCP-style reliability. NAT UDP mappings expire when quiet. For HTTP/3, pool QUIC connections, not "UDP connections."

TAKEAWAY

TCP pools connections. UDP reuses sockets (or QUIC on UDP).

How many sockets can you open (client vs server)

No fixed language max. Concurrent use is the lowest of FDs, ephemeral ports (when they apply), memory/threads, and app pools. Client and server hit different walls.

TCP

Limit ≈ min(pool, free FDs, free ephemeral ports) toward one remote IP:port. Typical ephemeral range ~16k (49152-65535).

SituationWhat happens
Many connects to same peerBound by free ephemeral ports + FDs
Connects to different remotesLocal ports can reuse across remotes (different 4-tuples)
Fast open/closeTIME_WAIT lowers usable ports
App pool tinyPool wins (e.g. Hikari 20) even if OS allows more

Server replies do not need a matching ephemeral port. Server speaks from the listen port (443) to the client's ephemeral port. Caps: FDs, backlog, accept/worker pool. When the server dials out (DB, API), that side is a client and uses ephemeral ports.

Same ephemeral port on two clients? OK if source IPs differ. Same client IP + same port + same server IP:port is the same 4-tuple → not allowed. NAT rewrites so home devices do not collide on the public side.


UDP

Usually one (or few) sockets, many datagrams. Do not burn one ephemeral port per request. Caps: FDs, buffers, NAT idle timeouts, app session tables. Server answers from the service port. Same "two clients, same source port" rule as TCP (4-tuple). HTTP/3 often shares one UDP socket with QUIC connection IDs inside.

TAKEAWAY

TCP client: pool + FDs + ephemeral ports per peer. TCP server: service port + FDs + workers. UDP: reuse sockets; do not size like one port per datagram.

Use cases

Use caseUsual transportWhy
HTTP/1.1, HTTP/2, HTTPS (classic)TCPReliable bytes; TLS on the stream
SSH, databases, SMTP, most APIsTCPOrdered, complete messages
DNS query (classic hot path)UDP/53Tiny Q&A; TCP for large/DNSSEC answers
NTP, many telemetry beaconsUDPOccasional loss OK
VoIP, live video, gamingUDPLate worse than lost
HTTP/3UDP (QUIC)Rebuild reliability on UDP

Bad picks: TCP for lossy real-time → lag; raw UDP for money APIs → silent loss; blaming the app for DNS timeouts that are UDP drops.

Reliability, latency, and failure modes

ConcernTCPUDP
LossRetransmit; app sees delayApp sees gap / timeout
LatencyHandshake + ACK/RTTFirst byte can be immediate
Head-of-lineGap blocks later data on the streamDatagrams independent
CongestionBacks offCan melt a link unless paced
MiddleboxesRST common on denyOften filtered / NAT timed out

Wire signals first: TCP retransmits, zero window, ECONNRESET, handshake timeout. UDP: ICMP unreachable (sometimes), one-way success rates, retry storms.

Why it still matters

Meshes and "HTTP everywhere" hide transport until loss shows up. Separate TCP failures, TLS failures, and app status. Ask: stream that must arrive complete and ordered, or message that can be lost and retried? If unclear, transport is unowned.

Final takeaway

"TCP reliable, UDP fast" is a poster. TCP is a stateful ordered byte stream. UDP is a thin datagram path. QUIC shows you can rebuild TCP-like guarantees on UDP when needed. Treat transport as a delivery control plane, same seriousness as DNS names and CDN cache keys.