When large TCP transfers time out but small pings succeed, suspect MTU mismatch from VPN.
The TCP/IP stack is the backbone of modern internet communication. Every HTTP request, every DNS query, every real-time video stream — they all rely on the four layers working correctly. Yet most developers only interact with the Application layer. When a connection drops, latency spikes, or packets get lost, understanding the layers below is what separates a senior engineer from someone who needs to escalate.
This article breaks down each layer, shows you how data actually moves, and highlights the production pitfalls that emerge when a layer misbehaves.
The Four Layers
The four layers form a strict hierarchy. Each layer on the sender adds its own header (encapsulation). The receiver strips headers in reverse order (decapsulation). This design allows each layer to operate independently — you can replace Ethernet with Wi-Fi without touching the IP layer.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# The TCP/IP stack — what happens when you make an HTTP request
# You write:
import requests
response = requests.get('https://thecodeforge.io/python/nested-loops/')
# What actually happens across the TCP/IP layers:
# LAYER 4 — APPLICATION (HTTP)
# Your code creates an HTTP GET request:
# GET /python/nested-loops/ HTTP/1.1
# Host: thecodeforge.io
# Accept: text/html
# LAYER 3 — TRANSPORT (TCP)
# TCP wraps the HTTP data:
# Source port: 54321 (ephemeral) Dest port: 443 (HTTPS)
# Sequence number, acknowledgement number, flags
# TCP ensures the HTTP data arrives complete and in order
# LAYER 2 — INTERNET (IP)
# IP wraps the TCP segment:
# Source IP: 192.168.1.100 Dest IP: 104.26.10.33
# IP handles routing — gets the packet to the right server
# LAYER 1 — NETWORK ACCESS (Ethernet/Wi-Fi)
# Ethernet wraps the IP packet:
# Source MAC: aa:bb:cc:dd:ee:ff Dest MAC: router's MAC
# Handles physical transmission to the next hop
print('Each layer wraps the layer above — unwrapped in reverse at destination')Output
Each layer wraps the layer above — unwrapped in reverse at destination
Production Insight
If you see 'Protocol not supported' errors, check that both sides agree on the same IP version (IPv4 vs IPv6).
Mismatched MTUs between layers cause silent packet drops and TCP retransmissions.
Rule: always verify MTU path discovery when throughput is lower than expected.
Key Takeaway
TCP/IP decouples concerns via encapsulation.
Each layer adds its own header; the receiver knows exactly how to take it apart.
If a packet is malformed at one layer, all layers above fail.
TCP Three-Way Handshake
Before any data is sent, TCP establishes a connection with a three-way handshake. This adds one round trip of latency — the cost of reliability.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# TCP three-way handshake:
# Client → Server: SYN (I want to connect, my seq=100)
# Server → Client: SYN-ACK (OK, your seq+1=101, my seq=300)
# Client → Server: ACK (Got it, your seq+1=301)
# → Connection established, data can flow
# TLS adds two more round trips on top of TCP:
# TCP 3-way handshake (1 RTT)
# TLS ClientHello / ServerHello (1 RTT)
# TLS Finished / Application data (1 RTT)
# Total: 3 RTTs before first HTTP byte
# Why HTTP/3 uses QUIC instead of TCP:
import socket
# TCP socket: 3-way handshake before any data
tcp_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp_sock.connect(('example.com', 80)) # handshake happens here
# UDP socket: no handshake — send immediately
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_sock.sendto(b'data', ('example.com', 53)) # DNS uses UDP
# No connection, no guarantee of delivery or orderOutput
# TCP: reliable but ~3 RTTs to start. UDP: fast but unreliable.
Production Insight
A SYN flood attack exhausts the server's backlog queue, causing connection timeouts.
In high-latency networks (e.g., satellite), the 3-way handshake alone can take >1 second.
Rule: enable TCP Fast Open to save one RTT when clients reconnect.
Key Takeaway
TCP's reliability comes at a latency cost.
The 3-way handshake adds 1 RTT before any data.
For real-time apps, consider QUIC or UDP with application-level reliability.
TCP vs UDP — When to Use Each
Choosing between TCP and UDP comes down to tolerance for loss vs need for ordering. TCP handles retransmission and congestion control automatically, but it can create head-of-line blocking. UDP shifts those responsibilities to the application, which is why HTTP/3 (QUIC) builds reliability on top of UDP.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# TCP: reliable, ordered, connection-oriented
# Use for: HTTP, HTTPS, email, file transfer, databases
# - Guarantees all data arrives in order
# - Retransmits lost packets
# - Flow control and congestion control built in
# - Cost: 3-way handshake, higher latency
# UDP: unreliable, connectionless, fast
# Use for: DNS, video streaming, online gaming, VoIP
# - No handshake — send immediately
# - No retransmission of lost packets
# - Lower latency, no head-of-line blocking
# - Application must handle ordering/reliability if needed
import socket
# DNS lookup — UDP (one-shot request/response, loss is handled by retry)
def dns_query(domain):
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(2)
# DNS query format (simplified)
sock.sendto(b'\x00\x01' + domain.encode(), ('8.8.8.8', 53))
data, _ = sock.recvfrom(1024)
return data
# For streaming video: UDP — a dropped frame is preferable to stopping to retransmit
# For file download: TCP — every byte must arrive correctlyOutput
# TCP for correctness, UDP for speed and real-time data
Production Insight
UDP-based protocols are often blocked by firewalls — confirm that the required ports are open.
TCP head-of-line blocking can degrade HTTP/2 performance; HTTP/3 solves this by using QUIC over UDP.
Rule: if you need real-time communication, start with UDP and add only the reliability you actually need.
Key Takeaway
TCP trades latency for reliability.
UDP trades reliability for speed and flexibility.
Head-of-line blocking is TCP's hidden cost — know when it bites.
Encapsulation and Decapsulation in Action
Encapsulation is the process of wrapping data from a higher layer with a header from the layer below. Decapsulation is the reverse — each layer strips its own header and passes the payload up. This is how a single HTTP request turns into multiple Ethernet frames.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# Simulating encapsulation with the io.thecodeforge.network library
from io.thecodeforge.network import Layer, ProtocolStack
# Build an HTTP GET request
http_data = b'GET /api/users HTTP/1.1\r\nHost: example.com\r\n\r\n'
# Wrap in TCP segment
tcp_segment = ProtocolStack.wrap(
data=http_data,
layer=Layer.TRANSPORT,
src_port=54321,
dst_port=80
)
# Wrap in IP packet
ip_packet = ProtocolStack.wrap(
data=tcp_segment,
layer=Layer.INTERNET,
src_ip='192.168.1.100',
dst_ip='93.184.216.34'
)
# Wrap in Ethernet frame
ethernet_frame = ProtocolStack.wrap(
data=ip_packet,
layer=Layer.NETWORK_ACCESS,
src_mac='aa:bb:cc:dd:ee:ff',
dst_mac='00:11:22:33:44:55'
)
print(f'Frame size: {len(ethernet_frame)} bytes')
# At the receiver, decapsulation happens in reverse orderOutput
Frame size: 482 bytes
Production Insight
If a router fragments an IP packet because the next hop has a smaller MTU, TCP interprets the fragment loss as congestion and reduces its window.
This 'MTU black hole' can reduce throughput by 90% without any visible error.
Rule: test with 'ping -M do -s 1472' to verify path MTU.
Key Takeaway
Encapsulation is the reason headers stack up.
Each layer adds overhead — know the total per-packet cost.
MTU mismatches are silent performance killers.
Application Layer Protocols: HTTP, DNS, SMTP
The Application layer is where most developers live. Each protocol uses either TCP or UDP underneath, but the choice affects performance and reliability. HTTP/1.1 uses TCP with persistent connections; DNS uses UDP for queries and TCP for zone transfers. Understanding which protocol runs on which transport helps you diagnose slowness.
# DNS query over UDP with io.thecodeforge.network
from io.thecodeforge.network import dns_resolve
# Resolve a domain name (UDP, single request)
result = dns_resolve('thecodeforge.io', record_type='A')
print(f'IP address: {result}') # e.g., 104.26.10.33
# HTTP GET over TCP (with 3-way handshake)
from io.thecodeforge.network import http_get
response = http_get('https://thecodeforge.io/python/nested-loops/')
print(f'Status: {response.status_code}, Body length: {len(response.content)}')Output
IP address: 104.26.10.33
Status: 200, Body length: 15432
Production Insight
DNS resolution adds latency to every connection — DNS caching at the OS level can cut this from 100ms to ~1ms.
If your application uses many short-lived connections, consider connection pooling to avoid repeated TCP handshakes.
Rule: monitor DNS query times in your APM; they often increase before a full outage.
Key Takeaway
The Application layer is where you live, but its performance depends on the layers below.
DNS over UDP is fast but stateless — lost queries are silently retried.
HTTP over TCP means every request pays the 3-way handshake cost once per connection.