< Back to Blog
#Network#TCP/IP#localhost

Behind localhost: A Network Journey That Never Leaves Home

Tech Sharing1940 words

AI translation notice: This English version was translated from the original Chinese article with AI assistance. Some nuances may differ from the original.

Starting with a Familiar Address

Type npm run dev in a terminal and Vite cheerfully tells you that the service is running at localhost:5173; click in a browser, and the page appears. Or, when working with Redis, you might run redis-cli -h 127.0.0.1 -p 6379, which also connects to the local machine. We know this scene very well. Recently, however, while reviewing computer networks for the 408 exam, I started studying network layering again.

That raised a natural question that is easy to overlook:

If I am connecting to “myself,” does this still count as network communication? Does it really pass through the network protocol stack? If so, how many layers does it go through?

A little investigation produced an answer that was slightly counterintuitive: yes, it counts, and it goes through quite a few layers. I wanted to write down the process from top to bottom and record what happens behind localhost.

In simple terms, it does not pass through a network card, switch, router, or cable the way an external request does. But inside the operating system, the application, transport, and network layers still do their jobs seriously. I used to think of localhost as “one program directly chatting with another,” but it is more like a parcel that never leaves its residential compound: it never gets on the highway, but the shipping label, sorting, and delivery process still happen.

One clarification first: localhost is a name, not an IP address. It usually resolves to the IPv4 loopback address 127.0.0.1; some systems may prefer the IPv6 address ::1. To keep the route clear, the discussion below assumes the IPv4 loopback path through 127.0.0.1.

Preface: Network Layering

Before everything begins, let us briefly review the network layers from the 408 curriculum so that we have a framework for the analysis that follows.

Network layers

A short story first:

The network layers are shown above. In 1977, the International Organization for Standardization (ISO) established a special committee to study a standard framework for network interconnection. It hoped to create a universal, open network-layering model so that any systems following the standard could communicate with one another. The reference model was formally published in 1984. It is the most standardized layering model, and almost any network can be fitted into it.

The story of TCP/IP begins with the Advanced Research Projects Agency (ARPA) of the United States Department of Defense.

  • In 1969, ARPANET was born. It was the predecessor of the modern Internet, initially intended to connect computers at universities and research institutions for resource sharing.
  • In 1974, Vint Cerf and Bob Kahn published a paper on TCP. It introduced the concept of a “gateway”—which later evolved into the router—and solved the problem of connecting different networks.
  • On January 1, 1983, ARPANET officially switched its core protocol from NCP to TCP/IP. This day is often regarded as the “birthday” of the modern Internet.
  • Berkeley later integrated a free implementation of TCP/IP into Unix through BSD Unix. As Unix spread through universities and research institutions, TCP/IP quickly spread with it.

Because TCP/IP was simpler, more efficient, and cheaper to implement, it naturally replaced the OSI approach and became today's dominant network-layering model. We will use the TCP/IP layers for the detailed discussion below.

Before continuing, here is a quick summary of what the five TCP/IP layers do:

  • The application layer is responsible for “what to say,” such as the HTTP, Redis, and MySQL protocols.
  • The transport layer is responsible for “which process receives it and how it is delivered.” TCP and UDP live here, as do port numbers.
  • The network layer is responsible for “which address to go to.” IP belongs here, so 127.0.0.1 is a network-layer address.
  • The link layer handles transmission over the same link. Ethernet frames and MAC addresses generally appear here.
  • The physical layer turns bits into electrical, optical, or wireless signals and actually moves data through physical media.

Based on that description, we can already analyze localhost:8080: 8080 is a port number belonging to the transport layer, while 127.0.0.1 is an IP address belonging to the network layer. Once these two concepts are separated, many networking questions become much less confusing.

TL;DR

Layer What happens Key point
Application layer The browser or client sends an HTTP request to 127.0.0.1:8080. This is where the data starts.
Transport layer The operating system uses port 8080 to find the corresponding service process, such as your web server, and encapsulates the data in a TCP segment. The port number takes effect here.
Network layer The operating system sees that the destination IP is 127.0.0.1, recognizes it as a loopback address, and hands it directly to the loopback interface instead of a physical network card. This is the crucial step: the data never leaves the machine.
Link layer The loopback interface simulates a virtual link layer and pretends to send the data over a network, but in reality it only makes an in-memory turn. There is no real MAC-address exchange.
Physical layer Skipped. No real hardware is involved, so the data is never turned into electrical or optical signals. This is the fundamental reason localhost is much faster than an external connection.

So localhost actually completes most of the process; it simply skips the physical layer.

Application Layer — No DNS Lookup

Let us begin at the top and see what the first step of curl localhost:6370 does.

Experiment: External Host vs. Localhost

In a terminal, I ran two commands: one accessing an external domain and one accessing my local service:

curl -v http://markxu.icu
curl -v localhost:6370

I captured the traffic with tcpdump:

sudo tcpdump -i any port 53

When accessing the external domain, the terminal showed output like this:

12:26:00.381970 IP 198.18.0.1.56714 > public1.114dns.com.domain: 53233+ A? markxu.icu. (28)
12:26:00.382370 IP public1.114dns.com.domain > 198.18.0.1.56714: 53233* 1/0/0 A 198.18.0.172 (44)
12:26:00.387550 IP 192.168.20.80.49570 > pdns.dnspod.cn.domain: 13415+ AAAA? markxu.icu. (28)
12:26:00.387656 IP 192.168.20.80.63675 > public1.alidns.com.domain: 13415+ AAAA? markxu.icu. (28)
12:26:00.387763 IP 192.168.20.80.52301 > public1.alidns.com.domain: 41468+ A? markxu.icu. (28)
12:26:00.388014 IP 192.168.20.80.56763 > pdns.dnspod.cn.domain: 41468+ A? markxu.icu. (28)

When accessing the local service, however, nothing appeared. That is because localhost is not an ordinary domain name. Under the IETF standard (RFC 6761), it is reserved as a special hostname. When the operating system handles localhost, it does not issue a DNS query; it reads the mapping directly from /etc/hosts instead.

You can verify this yourself:

cat /etc/hosts | grep localhost

localhost mapping

What Else Happens at the Application Layer?

Once the address is resolved, curl has the destination 127.0.0.1:6370. It then constructs an HTTP request:

GET / HTTP/1.1
Host: localhost:6370
User-Agent: curl/8.7.1
Accept: */*

This request has exactly the same format as one sent to an external host. The Host header says localhost:6370, but that is merely a difference in content; the protocol behavior is identical.

After constructing the request, curl writes it as a payload into the TCP socket associated with 127.0.0.1:6370. The application layer's work is complete, and the data moves down to the next layer.

Transport Layer — TCP Still Performs a Handshake

Once the application layer writes the HTTP message into the TCP socket, the transport layer takes over. This differed from what I had initially imagined:

Local communication still requires TCP's three-way handshake.

TCP is a connection-oriented protocol. Whether the two endpoints are remote machines or two processes on the same machine, it still performs the complete handshake: the client sends SYN, the server replies with SYN-ACK, and the client sends ACK. Only after the connection is established does the HTTP payload begin to travel.

The only difference is that the handshake does not cross any physical line. It completes almost instantly inside the operating-system kernel through the loopback interface lo. The RTT approaches zero, so there are no retransmissions and no congestion-control episode—but the protocol state machine, sequence numbers, acknowledgment numbers, and port numbers are all still present.

We can verify this with tcpdump. Open a terminal and listen on port 6370 of the loopback interface:

sudo tcpdump -i lo0 port 6370

Then run curl localhost in another terminal. The output looks like this:

tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on lo0, link-type NULL (BSD loopback), snapshot length 524288 bytes
14:17:36.505024 IP6 localhost.62464 > localhost.6370: Flags [S], seq 3529818479, win 65535, options [mss 16324,nop,wscale 6,nop,nop,TS val 2294771787 ecr 0,sackOK,eol], length 0
14:17:36.505080 IP6 localhost.6370 > localhost.62464: Flags [R.], seq 0, ack 3529818480, win 0, length 0
14:17:36.505206 IP localhost.62465 > localhost.6370: Flags [S], seq 1917119642, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 3300697293 ecr 0,sackOK,eol], length 0
14:17:36.505344 IP localhost.6370 > localhost.62465: Flags [S.], seq 4137660183, ack 1917119643, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 4162405884 ecr 3300697293,sackOK,eol], length 0
14:17:36.505365 IP localhost.62465 > localhost.6370: Flags [.], ack 1, win 6380, options [nop,nop,TS val 3300697293 ecr 4162405884], length 0
14:17:36.505379 IP localhost.6370 > localhost.62465: Flags [.], ack 1, win 6380, options [nop,nop,TS val 4162405884 ecr 3300697293], length 0
14:17:36.505607 IP localhost.62465 > localhost.6370: Flags [P.], seq 1:78, ack 1, win 6380, options [nop,nop,TS val 3300697293 ecr 4162405884], length 77
14:17:36.505631 IP localhost.6370 > localhost.62465: Flags [.], ack 78, win 6379, options [nop,nop,TS val 4162405884 ecr 3300697293], length 0
14:17:36.514104 IP localhost.6370 > localhost.62465: Flags [P.], seq 1:1448, ack 78, win 6379, options [nop,nop,TS val 4162405893 ecr 3300697293], length 1447
14:17:36.514168 IP localhost.62465 > localhost.6370: Flags [.], ack 1448, win 6358, options [nop,nop,TS val 3300697302 ecr 4162405893], length 0
14:17:36.514408 IP localhost.62465 > localhost.6370: Flags [F.], seq 78, ack 1448, win 6358, options [nop,nop,TS val 3300697302 ecr 4162405893], length 0
14:17:36.514434 IP localhost.6370 > localhost.62465: Flags [.], ack 79, win 6379, options [nop,nop,TS val 4162405893 ecr 3300697302], length 0
14:17:36.514744 IP localhost.6370 > localhost.62465: Flags [F.], seq 1448, ack 79, win 6379, options [nop,nop,TS val 4162405894 ecr 3300697302], length 0
14:17:36.514791 IP localhost.62465 > localhost.6370: Flags [.], ack 1449, win 6358, options [nop,nop,TS val 3300697303 ecr 4162405894], length 0
^C
14 packets captured
24 packets received by filter
0 packets dropped by kernel

Let us connect the lines one by one.

Please first adjust the code block above to use horizontal scrolling so that the line details are easier to read.

The Second Line: IPv6 Attempt (Rejected)

14:17:36.505024 IP6 localhost.62464 > localhost.6370: Flags [S], seq 3529818479, win 65535, options [mss 16324,nop,wscale 6,nop,nop,TS val 2294771787 ecr 0,sackOK,eol], length 0
14:17:36.505080 IP6 localhost.6370 > localhost.62464: Flags [R.], seq 0, ack 3529818480, win 0, length 0

The server on port 6370 was not listening on IPv6, so it immediately returned [R.]. This means that my service was bound to 127.0.0.1 (IPv4), not to ::1 (IPv6).

The Third Line: TCP Three-Way Handshake (IPv4)

14:17:36.505206 IP localhost.62465 > localhost.6370: Flags [S], seq 1917119642, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 3300697293 ecr 0,sackOK,eol], length 0
14:17:36.505344 IP localhost.6370 > localhost.62465: Flags [S.], seq 4137660183, ack 1917119643, win 65535, options [mss 16344,nop,wscale 6,nop,nop,TS val 4162405884 ecr 3300697293,sackOK,eol], length 0
14:17:36.505365 IP localhost.62465 > localhost.6370: Flags [.], ack 1, win 6380, options [nop,nop,TS val 3300697293 ecr 4162405884], length 0
  • curl switches to IPv4 (127.0.0.1), using source port 62465.
  • [S] = SYN (the client starts the connection).
  • [S.] = SYN-ACK (the server accepts the connection).
  • [.] = ACK (the client confirms it).

Notice the timestamps: all three lines are at 14:17:36.505, so the handshake completed within the same millisecond.

The lines in between contain the HTTP request and response.

The Sixth Line: TCP's Four-Way Termination

14:17:36.514408 IP localhost.62465 > localhost.6370: Flags [F.], seq 78, ack 1448, win 6358, options [nop,nop,TS val 3300697302 ecr 4162405893], length 0
14:17:36.514434 IP localhost.6370 > localhost.62465: Flags [.], ack 79, win 6379, options [nop,nop,TS val 4162405893 ecr 3300697302], length 0
14:17:36.514744 IP localhost.6370 > localhost.62465: Flags [F.], seq 1448, ack 79, win 6379, options [nop,nop,TS val 4162405894 ecr 3300697302], length 0
14:17:36.514791 IP localhost.62465 > localhost.6370: Flags [.], ack 1449, win 6358, options [nop,nop,TS val 3300697303 ecr 4162405894], length 0

The timestamps show just how quickly the process runs.

Network Layer — Routing Keeps It on This Machine

After the application layer passes the HTTP message to the TCP socket and the transport layer encapsulates it in a TCP segment, the data enters the network layer. Two things happen here: the IP header is added, and the routing table decides where the packet should go.

We can inspect how localhost appears at the network layer with:

route -n get 127.0.0.1

Network layer

  • The route destination for 127.0.0.1 is itself, and the interface is lo0; the flags include LOCAL.
  • The route destination for an external IP is its corresponding network segment. Here the interface is not the physical network card en0, but utun1024, because I had a proxy service enabled; the flags include GATEWAY.

Also note the MTU (Maximum Transmission Unit). The local upper limit is the macOS-configured value of 16,384, while the connection to the external network uses the standard Ethernet limit of 1,500.

Link Layer — A Virtual “Pretend Transmission”

Once the network layer has made its routing decision, the packet is marked as “destined for this host.” In a normal network flow, the link layer would next encapsulate it in an Ethernet frame, fill in the destination MAC address, and hand it to the physical layer.

For 127.0.0.1, however, things become unusual here.

No Real MAC-Address Exchange

We can verify this with the arp command:

arp -a | grep 127.0.0.1

There is no output. For a loopback address, the operating system does not send an ARP request asking, “Who owns 127.0.0.1? Tell me your MAC address.”

By contrast, if you look up another device on the local network:

arp -a | grep 192.168

Comparing MAC addresses

What Does the Loopback Interface Do?

The operating system contains a special virtual network interface called the loopback interface, usually lo0 on macOS and Linux. You can see that it exists with:

ifconfig lo0

lo0

The key information is:

  • LOOPBACK: identifies this as a loopback interface.
  • inet 127.0.0.1: shows that the address is bound to it.
  • mtu 16384: the maximum transmission unit is much larger than Ethernet's 1,500.

When the network layer decides to send a packet to 127.0.0.1, what actually happens is this: it hands the packet to the virtual interface lo0. Instead of calling a network-card driver, lo0 directly “mirrors” the packet into the receive queue of the operating system's network stack. From the receiver's point of view, it looks as though a packet has arrived from the network.

This process is called loopback inside the operating system. At its core, it is an in-memory copy, with no hardware involved.

Does the Link Layer Still Exist?

Strictly speaking, the link layer is responsible for “transmitting frames over the same link.” For the loopback interface, the operating system simulates an extremely simple link layer. It adds a fake link-layer header (NULL on macOS and LOOPBACK on Linux), but it does not add real MAC addresses or perform CSMA/CD (Carrier Sense Multiple Access with Collision Detection). There is therefore no actual frame-transmission delay.

What About the Physical Layer?

It is skipped entirely. It is nice not to have to write this section at all (^∇^)

The Answer: Localhost Passes Through Four of the Five TCP/IP Layers

The only layer it skips is the physical layer.

It is not as simple as “one program directly chats with another.” It still goes through the whole process of protocol encapsulation, port addressing, routing decisions, and interface scheduling. The only difference is that everything happens inside one machine—like a parcel that never leaves its residential compound: the label is attached, the parcel is sorted, and the delivery is signed for, but the courier never reaches the road.

Copyright
  • Author:Mark Xu
  • Copyright:All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating otherwise.

© 2026 Mark Xu. All rights reserved.

All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating otherwise.

A small site built with React + Vite + Tailwind