A step-by-step breakdown of shell parsing, ELF linking, glibc resolver queries, non-blocking TCP handshakes, socket buffers, and the infamous 301 redirect.
You type curl google.com in your terminal and hit enter. Within forty milliseconds, a short snippet of HTML lands on your screen:
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>
Most engineers glance at that output and assume curl made a quick HTTP call and printed the response.
That explanation skips almost everything interesting.
Between the moment your finger depresses the enter key and the moment your prompt returns, your machine moves through dozens of discrete subsystems. Bash parses raw keystrokes from a pseudo-terminal. The kernel loads an ELF binary and resolves dynamic libraries through memory maps. The GNU C library queries the Name Service Switch to find out how your machine translates hostnames. A non-blocking TCP socket initiates a three-way handshake across network queues and physical network interfaces.
Then Google rejects your request, sends back a redirect, and curl tears down the socket before you can blink.
Here is the entire mechanical chain, traced down through the system calls, kernel structures, and network packets.
1. The Terminal, the Shell, and the Execve Boundary
Everything begins before curl even exists in memory.
When you type on your keyboard, your terminal emulator (like Alacritty, GNOME Terminal, or Windows Terminal over SSH) sends scan codes across a pseudo-terminal pair (/dev/pts/X). The shell process (usually Bash) sits blocked inside a read() system call on file descriptor 0.
# You can see bash waiting for your input:
read(0, "c", 1) = 1
read(0, "u", 1) = 1
read(0, "r", 1) = 1
read(0, "l", 1) = 1
read(0, " ", 1) = 1
read(0, "g", 1) = 1
...
read(0, "\n", 1) = 1
Once the newline byte \n arrives, Bash wakes up. It reads the string into its internal line buffer and begins tokenization.
First, Bash checks for aliases. If you aliased curl to curl -i or curl -s in your .bashrc, Bash substitutes the command string immediately. Next, it looks for shell functions, built-ins, and reserved keywords. None match.
Bash now knows it needs to execute an external binary. To find where that binary lives on disk, it checks its internal command hash table. If you have run curl recently, Bash remembers the exact path /usr/bin/curl. If not, it iterates through the colon-separated directories in your $PATH environment variable:
stat("/usr/local/sbin/curl", ...) = -1 ENOENT (No such file or directory)
stat("/usr/local/bin/curl", ...) = -1 ENOENT (No such file or directory)
stat("/usr/sbin/curl", ...) = -1 ENOENT (No such file or directory)
stat("/usr/bin/curl", {st_mode=S_IFREG|0755, st_size=281808, ...}) = 0
Bash finds the executable at /usr/bin/curl.
Now comes the process fork. Bash cannot replace its own running image because it must survive to draw the next prompt. It calls clone() (or fork() on older kernels) with flags like SIGCHLD to spawn an identical child process.
The child process inherits file descriptors 0 (stdin), 1 (stdout), and 2 (stderr), all pointing back to your pseudo-terminal slave. Immediately after cloning, the child calls execve():
execve("/usr/bin/curl", ["curl", "google.com"], [/* 48 vars */]) = 0
The shell image in the child process is instantly overwritten. Bash in the parent process calls wait4() and puts itself to sleep, waiting for the child to finish.
2. The ELF Loader and Dynamic Shared Libraries
The Linux kernel takes control of the new process. It opens /usr/bin/curl and inspects the first four bytes of the file. They must match the standard ELF magic number: 0x7f 0x45 0x4c 0x46 (\x7fELF).
Because curl is compiled as a dynamically linked binary, the kernel does not jump directly into curl's main() function. Instead, it reads the PT_INTERP program header from the ELF file. This header points to the dynamic linker:
/lib64/ld-linux-x86-64.so.2
The kernel sets up the process stack with arguments (argv), environment variables (envp), and auxiliary vectors (which provide details like hardware capabilities and random seeds for ASLR). Then it maps the dynamic linker into memory and transfers control to it.
The dynamic linker inspects the DT_NEEDED tags in curl's ELF binary to find every shared library curl relies on. On a typical modern Linux distribution, curl depends on several heavy shared objects:
ldd /usr/bin/curl
The output reveals the dependency graph:
-
libcurl.so.4(the core transfer engine) -
libssl.so.3andlibcrypto.so.3(OpenSSL cryptographic routines) -
libz.so.1(compression support) -
libc.so.6(standard C runtime)
The dynamic linker maps each of these shared libraries into the process's virtual memory address space using mmap() system calls. It applies relocations, binds symbol addresses, sets memory page protections (marking code pages as read-only and executable), and activates security protections like Full RELRO and stack canaries.
Once dynamic linking finishes, control finally jumps to _start inside curl, which initializes the C runtime and calls main().
3. Argument Parsing and the Easy Handle
Curl starts by parsing its command-line arguments using getopt or internal string parsers.
The argument passed was google.com.
Notice what is missing: there is no URL scheme. You did not specify http:// or https://.
What does curl do? By default, curl inspects the string. It checks if the string starts with a recognized scheme followed by ://. Finding none, curl prepends http:// as the default protocol (though newer versions allow configuring --proto-default).
Because the default protocol is HTTP, curl sets the target destination port to 80.
Curl now allocates its central operational structure: the Curl_easy handle. This structure holds state machines, timeout values, buffer pointers, error buffers, and protocol handlers.
Curl also checks whether its standard output is connected to a terminal by calling isatty(1). If stdout is a terminal, curl configures itself to show transfer progress meters if requested, but hides them for quick responses to prevent cluttering your prompt.
No network packets have moved yet. Everything so far has been local CPU cycles and memory mapping.
4. The Domain Name Resolution (DNS) Pipeline
Curl has a hostname (google.com), but network routers only understand IP addresses. Curl must translate google.com into an IPv4 or IPv6 address.
Curl does not contain hardcoded DNS packet builders. Instead, it relies on the C library by calling the standard POSIX resolver function:
getaddrinfo("google.com", "http", &hints, &res)
This single call triggers an extensive internal investigation inside the Linux operating system.
Step 4A: The Name Service Switch (/etc/nsswitch.conf)
The C library (glibc) opens /etc/nsswitch.conf to check how this specific machine is configured to resolve hosts. It looks for the hosts: line:
hosts: files mdns4_minimal [NOTFOUND=return] dns
This configuration tells glibc the order of operations:
- Check local static files first (
files). - If not found, attempt multicast DNS (
mdns4_minimal). - If not found, query standard DNS servers (
dns).
Step 4B: Checking Static Files (/etc/hosts)
glibc opens /etc/hosts and scans it line by line using openat() and read(). It checks if someone mapped google.com to a static IP address like 127.0.0.1.
Unless you are actively intercepting traffic for local development, google.com is not in /etc/hosts. glibc closes the file and moves to the next provider.
Step 4C: Reading the Resolv Configuration (/etc/resolv.conf)
Now glibc needs to know which DNS server to contact. It opens /etc/resolv.conf. On modern systemd-based Linux installations (Ubuntu, Fedora, Debian), you will usually see:
nameserver 127.0.0.53
options edns0 trust-ad
127.0.0.53 is not an external server. It is a local loopback IP address managed by systemd-resolved, a local caching and DNS multiplexing daemon.
Step 4D: The DNS Query on the Wire
glibc creates a UDP socket using socket(AF_INET, SOCK_DGRAM|SOCK_CLOEXEC, 0) and calls connect() to attach the socket to 127.0.0.53:53.
It constructs a DNS query packet asking for two records:
- An A record (for IPv4)
- An AAAA record (for IPv6)
It transmits the packet using sendto():
sendto(sockfd, "\x12\x34\x01\x00\x00\x01\x00\x00\x00\x00\x00\x00\x06google\x03com\x00\x00\x01\x00\x01", 28, 0, ...)
The systemd-resolved service catches this request. If it already has the answer in its local in-memory cache, it replies in less than one millisecond.
If the entry is not cached, systemd-resolved looks at its upstream network interfaces (configured via DHCP or static cloud configurations like AWS Route 53 or Google Cloud DNS at 169.254.169.254 or public DNS like 8.8.8.8). It sends an outbound UDP DNS request across your primary network interface, awaits the upstream answer, caches the result, and writes the response back down to the loopback socket where glibc is waiting in poll().
glibc reads the response using recvfrom(). It parses the DNS answers and hands curl a linked list of addrinfo structures containing resolved IP addresses:
- IPv4:
142.250.190.46 - IPv6:
2607:f8b0:4009:81b::200e
Step 4E: The "Happy Eyeballs" Race (RFC 8305)
Modern versions of curl implement Happy Eyeballs.
If both IPv6 and IPv4 addresses are returned, curl does not just pick one and freeze if your local IPv6 routing is broken. It attempts to connect to the IPv6 address first. If the IPv6 connection does not complete within approximately 250 milliseconds, curl simultaneously launches a connection attempt to the IPv4 address. Whichever connection establishes first wins, and curl cancels the slower attempt.
Let us trace the common IPv4 connection to 142.250.190.46:80.
5. Routing, ARP, and Layer 2 Discovery
Curl now has an IP address (142.250.190.46) and a destination port (80). But the Linux kernel cannot just spray packets into thin air. It needs to know which physical interface to use and which local hardware address to target.
The kernel consults its Forwarding Information Base (FIB), commonly known as the routing table:
ip route get 142.250.190.46
The kernel's routing subsystem evaluates the destination IP against its routes:
142.250.190.46 via 192.168.1.1 dev eth0 src 192.168.1.50 uid 1000
cache
This kernel lookup tells the networking stack three crucial things:
- The packet must exit via interface
eth0. - The local source IP address to stamp into the IP header is
192.168.1.50. - The destination is not on the local subnet, so the packet must be sent to the default gateway at
192.168.1.1.
Resolving the Gateway MAC via ARP
Ethernet switches do not understand IP addresses. They only route Ethernet frames based on 48-bit MAC addresses.
The kernel checks its internal ARP neighbor cache (ip neigh). It looks for an entry for 192.168.1.1.
If the entry is present (REACHABLE), the kernel grabs the gateway MAC address (for example, 00:1a:2b:3c:4d:5e).
If the entry has expired or is missing, the kernel pauses transmission and broadcasts an ARP request frame to the entire local broadcast domain:
ARP, Request who-has 192.168.1.1 tell 192.168.1.50, length 28
Your local home router or top-of-rack data center switch hears the broadcast and replies directly with an ARP reply:
ARP, Reply 192.168.1.1 is-at 00:1a:2b:3c:4d:5e, length 28
The Linux kernel saves this mapping in its ARP table. Now it has everything required to build a physical Ethernet frame.
6. The Non-Blocking TCP Three-Way Handshake
Curl creates the actual communication socket:
int fd = socket(AF_INET, SOCK_STREAM|SOCK_NONBLOCK|SOCK_CLOEXEC, IPPROTO_TCP);
Notice the SOCK_NONBLOCK flag. Curl never uses blocking sockets. If a network call blocks, the entire process freezes. By using non-blocking I/O, curl keeps total control over timeouts and event loops.
Curl calls connect():
connect(fd, {sa_family=AF_INET, sin_port=htons(80), sin_addr=inet_addr("142.250.190.46")}, 16) = -1 EINPROGRESS
Because the socket is non-blocking, connect() cannot finish immediately. It returns -1 with an errno of EINPROGRESS. This is completely normal in network programming. It simply means: "The handshake has started in the background; check back in a moment."
Inside the Linux kernel, the TCP engine springs into action.
The SYN Packet
The kernel allocates a socket buffer structure (struct sk_buff, commonly called an skb). It builds the TCP packet:
- Source port: An ephemeral port chosen randomly from
/proc/sys/net/ipv4/ip_local_port_range(e.g.,48212). - Destination port:
80. - Sequence number: A cryptographically randomized 32-bit initial sequence number (ISN).
- Flags:
SYNflag set. - TCP Options: Maximum Segment Size (MSS), Selective Acknowledgments (SACK Permitted), Window Scale factor, and TCP Timestamps.
The kernel wraps the TCP segment inside an IPv4 packet, sets the TTL to 64, and places it inside an Ethernet frame stamped with the destination MAC of your gateway.
The frame is pushed into the network interface transmission queue (qdisc, like fq_codel). The network driver writes the descriptor to the physical network card's TX ring buffer using Direct Memory Access (DMA). The network card transmits the electrical or optical signal across the wire.
The socket transitions to the SYN_SENT state.
The SYN-ACK Arrival
Across the internet, routers forward the packet to the nearest Google edge POP (Point of Presence). Google's edge load balancer (the Google Front End, or GFE) accepts the SYN and replies with a SYN-ACK packet.
When the electrical signals return to your local network card:
- The NIC copies the incoming frame into a host RAM buffer via DMA.
- The NIC fires a hardware interrupt (IRQ) to a CPU core.
- The kernel interrupt handler schedules a SoftIRQ (software interrupt) via the NAPI (New API) polling subsystem.
- The kernel polls the RX ring buffer, wraps the frame in an
skb, and feeds it intonetif_receive_skb(). - The TCP/IP stack strips the Ethernet and IP headers, validates the TCP checksum, matches the packet against your socket's 4-tuple (source IP, source port, dest IP, dest port), and finds the socket waiting in
SYN_SENT. - The kernel updates the socket state to
ESTABLISHED. - The kernel automatically generates and sends the final
ACKpacket back to Google, completing the three-way handshake.
Waking Curl
While the kernel was handling packets, curl was waiting inside poll() or ppoll():
ppoll([{fd=3, events=POLLOUT|POLLWRNORM}], 1, {tv_sec=299, tv_nsec=999000000}, NULL, 8) = 1
The moment the socket transitions to ESTABLISHED, the kernel marks the socket descriptor as writable. poll() wakes up. Curl calls getsockopt(fd, SOL_SOCKET, SO_ERROR, ...) to confirm that the connection succeeded without errors.
The TCP tunnel is open.
7. Sending the HTTP Request
Now that the transport layer is established, curl moves to Layer 7 (the application layer).
Curl builds the HTTP request in an internal heap buffer. Because curl defaults to HTTP/1.1 for plain web requests, the buffer contains a simple ASCII string:
GET / HTTP/1.1\r\n
Host: google.com\r\n
User-Agent: curl/8.5.0\r\n
Accept: */*\r\n
\r\n
Notice the components of this request:
-
GET /: The requested path is root. -
Host: google.com: Mandatory in HTTP/1.1 so multi-tenant reverse proxies know which virtual host you want. -
User-Agent: curl/8.5.0: Curl identifies itself and its version. -
Accept: */*: Curl informs the server that it will accept any MIME type in response. - An empty line (
\r\n\r\n): Signals the end of the HTTP headers.
Curl writes this buffer into the socket using sendto():
sendto(3, "GET / HTTP/1.1\r\nHost: google.com\r\nUser-Agent: curl/8.5.0\r\nAccept: */*\r\n\r\n", 75, 0, NULL, 0) = 75
The sendto() syscall does not send the packet across the internet immediately.
Instead, it copies those 75 bytes from curl's user space memory into the socket's kernel write buffer (sk_write_queue). The kernel packetizes the data, applies TCP segmentation, calculates checksums (often offloaded to the physical NIC via TCP Segmentation Offload, or TSO), and flushes the packet to the transmission ring buffer.
Curl now transitions from writing to reading. It calls poll() again, this time watching for POLLIN (incoming data readable).
8. Google's Edge and the Infamous 301 Redirect
Your request reaches Google's edge infrastructure on port 80.
Google does not serve its search engine over unencrypted HTTP. Serving plain HTTP would expose queries, cookies, and user sessions to eavesdropping and ISP injection.
However, Google cannot simply drop port 80 traffic. If it did, users typing google.com into their browsers would experience connection timeouts. Port 80 exists exclusively to bounce clients over to secure transport.
Google's server software processes your request and crafts an HTTP response:
HTTP/1.1 301 Moved Permanently
Location: http://www.google.com/
Content-Type: text/html; charset=UTF-8
Date: Sun, 25 Oct 2026 05:14:22 GMT
Expires: Tue, 24 Nov 2026 05:14:22 GMT
Cache-Control: public, max-age=2592000
Server: gfe
Content-Length: 219
Take a close look at that Location header. It says:
Location: http://www.google.com/
This surprises almost everyone. Why did Google redirect plain http://google.com to http://www.google.com/ instead of jumping straight to https://?
Google maintains strict canonical domain rules. The apex domain google.com first normalizes to the www. subdomain. If you follow that redirect, http://www.google.com/ will issue a second redirect to https://www.google.com/.
Google also appends a tiny fallback HTML payload to the response body:
<HTML><HEAD><meta http-equiv="content-type" content="text/html;charset=utf-8">
<TITLE>301 Moved</TITLE></HEAD><BODY>
<H1>301 Moved</H1>
The document has moved
<A HREF="http://www.google.com/">here</A>.
</BODY></HTML>
Why include HTML inside a 301 response? For ancient HTTP/1.0 clients or web crawlers that do not automatically follow Location headers, this embedded link allows a human or robot to manually click through to the new address.
9. Receiving the Bytes and Writing to stdout
The packet containing Google's 301 response travels back through your ISP, hits your router, enters your network card, and passes through the kernel's NAPI receive pipeline.
The kernel places the payload into the socket's receive buffer and wakes up curl's waiting poll() call.
Curl calls recv():
recv(3, "HTTP/1.1 301 Moved Permanently\r\nLocation: http://www.google.com/\r\n...", 16384, 0) = 542
Curl reads 542 bytes into its memory buffer.
Curl's HTTP state machine parses the response line: HTTP/1.1 301 Moved Permanently. It parses the headers line by line until it hits the double CRLF (\r\n\r\n).
Here is where standard curl behavior surprises people: curl does not follow redirects by default.
In a web browser, a 301 status code triggers an immediate internal navigation to the URL in the Location header. But curl is a UNIX command-line tool. UNIX tools do not make assumptions about what you want. Unless you explicitly pass the -L or --location flag, curl treats a 301 response as a finished transfer.
By default, curl hides HTTP response headers and only outputs the payload body.
Curl finds the start of the HTML body in its buffer. It prepares to print it. It calls the standard write() system call on file descriptor 1 (stdout):
write(1, "<HTML><HEAD><meta http-equiv=\"co"..., 219) = 219
Because file descriptor 1 points to your pseudo-terminal slave device (/dev/pts/X), the Linux line discipline receives the bytes. The line discipline passes the raw ASCII text across the pseudo-terminal master descriptor to your terminal emulator process.
Your terminal emulator rasterizes the fonts, maps the glyphs against its font cache, submits render draw calls to your graphics server (via Wayland or X11), and the HTML snippet appears on your monitor.
10. Connection Teardown and Process Destruction
The transfer is complete. Curl begins its shutdown routine.
First, it closes the network connection:
close(3) = 0
Inside the kernel, closing the socket file descriptor decrements its reference count to zero. The kernel initiates a clean TCP four-way handshake. It sends a FIN packet to Google's server. Google replies with an ACK, sends its own FIN, and your kernel replies with a final ACK.
Your local TCP socket enters the TIME_WAIT state.
Why TIME_WAIT? The kernel keeps the socket parameters alive for twice the Maximum Segment Lifetime (2 * MSL, typically 60 seconds on Linux) to ensure any delayed or retransmitted packets wandering through the internet do not accidentally collide with a newly opened socket using the same port.
Next, curl frees its memory structures. It deallocates the Curl_easy handle, unloads its protocol state machines, and calls exit_group(0):
exit_group(0)
The kernel reclaims the process's page tables, unmaps the shared libraries, and frees virtual memory pages. The process descriptor enters the EXIT_ZOMBIE state.
The Linux kernel delivers a SIGCHLD signal to the parent process: Bash.
Bash wakes up from its wait4() call:
wait4(28419, [{WIFEXITED(s) && WEXITSTATUS(s) == 0}], 0, NULL) = 28419
Bash collects curl's exit status (0, indicating success). The kernel cleans up the remaining zombie process entry from the process table.
Bash issues another read() call to wait for your next command, prints its prompt string (PS1), and sits waiting for your next keystroke.
The entire sequence took less than fifty milliseconds.
The Practical Diagnostic Arsenal: Tracing It Yourself
You do not have to take any of this on faith. You can observe every single layer of this process on any Linux server using three standard diagnostic tools.
1. Tracing the System Calls (strace)
To see the exact boundary where curl interacts with the Linux kernel, run curl through strace:
strace -f -e trace=process,network,openat,read,write curl -s google.com > /dev/null
You will see the exact sequence: execve, dynamic library mapping, opening /etc/resolv.conf, creating the UDP socket for DNS, the non-blocking connect() returning EINPROGRESS, poll() waiting for readiness, and the final write() to file descriptor 1.
2. Measuring Internal Phase Latency with Curl
Curl contains a built-in formatting engine that extracts microsecond-level timing for every internal phase. Save this snippet in your shell:
curl -w "\
DNS Lookup Time : %{time_namelookup}s\n\
TCP Connect Time : %{time_connect}s\n\
App Connect (TLS) : %{time_appconnect}s\n\
Pre-Transfer Time : %{time_pretransfer}s\n\
Start Transfer : %{time_starttransfer}s\n\
Total Time : %{time_total}s\n\
HTTP Status Code : %{http_code}\n\
Redirect URL : %{redirect_url}\n" \
-o /dev/null -s google.com
Running this command against google.com displays the exact latency profile of your request:
DNS Lookup Time : 0.004120s
TCP Connect Time : 0.018450s
App Connect (TLS) : 0.000000s
Pre-Transfer Time : 0.018510s
Start Transfer : 0.034210s
Total Time : 0.034320s
HTTP Status Code : 301
Redirect URL : http://www.google.com/
Notice that time_appconnect is exactly 0.000000s. Because the request was plain HTTP, zero TLS handshakes occurred.
3. Watching the Wire with tcpdump
Open a second terminal window and run tcpdump on your active interface:
sudo tcpdump -i any -nn "port 80 or port 53"
Then run curl google.com in your primary window. You will see the raw UDP packets heading to port 53 for DNS, followed by the TCP SYN, SYN-ACK, ACK handshake on port 80, the HTTP GET push, the 301 response, and the FIN teardown.
Exactly What Happens When You Pass -L
If you want the real Google search homepage, you must instruct curl to follow redirects using the -L (or --location) flag:
curl -L google.com
When you pass -L, the internal process repeats twice more:
First, curl reads the Location: http://www.google.com/ header. It tears down the first TCP connection, resolves www.google.com, connects on port 80, and issues another GET.
Google's server on www.google.com:80 responds with a second redirect:
HTTP/1.1 302 Found
Location: https://www.google.com/
Curl catches the 302, notes the change in protocol scheme from http:// to https://, changes the target port to 443, resolves the address again, opens a brand new TCP connection, negotiates an OpenSSL TLS 1.3 handshake (exchanging ClientHello, ServerHello, cryptographic certificates, and session keys), and finally issues an encrypted HTTP/2 or HTTP/1.1 GET / request.
Only then does Google stream back the full, multi-kilobyte JavaScript and HTML payload of its actual search page.
An Interesting Fact About Curl
Curl was not created as a general-purpose web debugging tool. In 1996, Swedish developer Daniel Stenberg was maintaining an IRC bot called Fraggel. He wanted the bot to automatically fetch currency exchange rates from web pages once a day so IRC users could convert Swedish Kronor to US Dollars in chat rooms.
Finding no lightweight CLI tool that fit his needs, he adopted an open-source utility called httpget, patched it, added FTP support, and renamed it urlget. On March 20, 1998, after adding upload support, he officially released it under the name cURL (Client URL).
Today, Daniel Stenberg still actively maintains the project, and libcurl runs on an estimated twenty billion devices worldwide, including Mars rovers, smart televisions, cars, gaming consoles, and virtually every server running in the cloud.
Summary: The Complete Execution Path
When you strip away the abstraction, a single curl google.com command traverses ten clear boundaries:
-
Terminal: Readline captures keystrokes over
/dev/pts/X. -
Shell: Bash expands aliases, locates
/usr/bin/curlvia$PATH, forks, and callsexecve(). -
Linker:
ld.somapslibcurl,libssl, andlibc, activating ASLR and page protections. -
Initialization: Curl parses arguments, defaults to HTTP port 80, and allocates
struct Curl_easy. -
Resolver: glibc checks
/etc/nsswitch.conf, skips/etc/hosts, and sends UDP queries to127.0.0.53. - Network Layer: The kernel checks the FIB routing table, finds the default gateway, and resolves the gateway MAC via ARP.
-
Transport Layer: A non-blocking TCP socket sends a SYN packet, handles SYN-ACK via SoftIRQ, and reaches
ESTABLISHED. - Application Layer: Curl formats a plaintext HTTP/1.1 GET request and writes it to the socket buffer.
- Edge Response: Google Front End rejects plain HTTP and returns a 219-byte 301 Moved Permanently payload.
-
Terminal Output: Curl reads the payload, writes it to stdout (fd 1), closes the socket to enter
TIME_WAIT, and exits to let Bash draw the next prompt.
Knowing these layers turns mystical network failures into predictable troubleshooting steps. If curl hangs on a server, you no longer guess. You test DNS with dig, check TCP routing with ip route get, inspect socket states with ss -t, and trace syscall bottlenecks with strace.
What is the most surprising layer you discovered when digging into network commands on Linux?
If this breakdown saved you hours of debugging or gave you something practical to use in production, consider buying me a coffee. Your support directly fuels independent, zero-fluff Linux and DevOps technical guides.
About the Author
Asep Sayyad is a Linux and DevOps engineer passionate about Linux administration, automation, cloud technologies, containers, and open-source software. He enjoys solving real-world infrastructure challenges and sharing practical knowledge through in-depth technical articles, tutorials, and hands-on guides.
His goal is to help aspiring and experienced engineers build stronger Linux and DevOps skills with content focused on real production scenarios rather than theory alone.
Connect with Me
- Portfolio: asepsayyad007.in
- Blog: asepsayyad007.in/blogs
- GitHub: github.com/asepsayyad007
- LinkedIn: linkedin.com/in/asepsayyad
- Medium: asepsayyad007.medium.com
- Support: buymeacoffee.com/asepsayyad007
Enjoyed this article?
If this guide saved you hours of debugging or gave you something practical for production, consider:
- Buying me a coffee: Your support directly fuels independent, zero-fluff Linux and DevOps engineering breakdowns.
- Starring my open-source projects on GitHub.
- Sharing this article with fellow Linux and DevOps engineers.
You can also follow me for more practical content on Linux, DevOps, Cloud, Containers, Automation, and Open Source. Thanks for reading, and enjoy your learning!
© 2026 Asep Sayyad
Top comments (1)
The DNS step is the one I keep coming back to, because on a systemd machine the nameserver in resolv.conf is 127.0.0.53, not a real upstream, so a tool that queries DNS directly can disagree with what curl sees through glibc. In my own debugging I once chased a TCP timeout that turned out to be a stale local resolver cache, not the server. The 301 detail matters too: without -L, curl stops at the www redirect and never reaches https. When Happy Eyeballs races IPv6 and IPv4, do you see it fall back cleanly on networks where IPv6 is advertised but broken?