<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: vbcloud</title>
    <description>The latest articles on DEV Community by vbcloud (@vbcloud).</description>
    <link>https://dev.to/vbcloud</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4237899%2Fb9e9e93c-2f42-454f-a623-11fa196f1e43.png</url>
      <title>DEV Community: vbcloud</title>
      <link>https://dev.to/vbcloud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vbcloud"/>
    <language>en</language>
    <item>
      <title>What actually happens when you type a URL and hit enter</title>
      <dc:creator>vbcloud</dc:creator>
      <pubDate>Tue, 10 Nov 2026 21:23:24 +0000</pubDate>
      <link>https://dev.to/vbcloud/what-actually-happens-when-you-type-a-url-and-hit-enter-5cg0</link>
      <guid>https://dev.to/vbcloud/what-actually-happens-when-you-type-a-url-and-hit-enter-5cg0</guid>
      <description>&lt;p&gt;I traced one curl command through every layer, on a VM I was allowed to break. Every output below is real, trimmed for length.&lt;/p&gt;

&lt;p&gt;If you can type a URL but couldn't say what happens in the second before the page appears, this post is for you. By the end you should be able to read a packet capture of a web request, say which step each line belongs to, and make a sensible first guess when one of those steps fails.&lt;/p&gt;

&lt;p&gt;I'm a few weeks into learning DevOps. The setup is a small Ubuntu VM, plus dig, tcpdump and curl.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The URL says what to ask, and who to ask&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://example.com/index.html" rel="noopener noreferrer"&gt;https://example.com/index.html&lt;/a&gt; carries three separate instructions:&lt;/p&gt;

&lt;p&gt;The scheme (https) says how to talk. It also implies a port: 443 for HTTPS, 80 for plain HTTP.&lt;br&gt;
The host (example.com) says who to talk to. It's a name for humans. The network moves packets between addresses, not names.&lt;br&gt;
The path (/index.html) says what you want. Only the server decides what that means.&lt;/p&gt;

&lt;p&gt;The host is the first problem to solve, because nothing can be sent until the name becomes an address.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DNS: turning a name into an address&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;On my VM, programs don't resolve names themselves. They ask a local stub resolver at 127.0.0.53, which forwards the question to an upstream resolver (here 75.75.75.75, handed out by DHCP). That resolver does the walking, and it caches what it finds.&lt;/p&gt;

&lt;p&gt;You can do the walk yourself. dig example.com +trace skips the cache and starts at the root:&lt;/p&gt;

&lt;p&gt;;; Received 811 bytes from 127.0.0.53#53(127.0.0.53) in 45 ms&lt;br&gt;
;; Received 836 bytes from 199.7.83.42#53(l.root-servers.net) in 37 ms&lt;br&gt;
;; Received 359 bytes from 2001:502:1ca1::30#53(e.gtld-servers.net) in 620 ms&lt;br&gt;
example.com.            300     IN      A       172.66.147.243&lt;br&gt;
example.com.            300     IN      A       104.20.23.154&lt;br&gt;
;; Received 72 bytes from 2803:f800:50::6ca2:c0a2#53(hera.ns.cloudflare.com) in 23 ms&lt;/p&gt;

&lt;p&gt;Read it top to bottom. The root server doesn't know example.com, but it knows who runs .com. The .com server doesn't know the address either, but it knows which nameservers are responsible for example.com. Those nameservers (here Cloudflare's) finally give the answer. Roots and top-level servers only hand out referrals. Only the authoritative server answers.&lt;/p&gt;

&lt;p&gt;The 300 is the TTL: "you may keep this for up to 300 seconds." Two details I wouldn't have guessed:&lt;/p&gt;

&lt;p&gt;Every name is asked twice at the same moment, once for an IPv4 address (A) and once for IPv6 (AAAA).&lt;br&gt;
The TTL counts down in a cache. I asked the same question twice a few seconds apart and got 68, then 59: the same record, aging. The authoritative server always says 300 and marks its answer with the aa flag.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Is it local?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now I have an address, 104.20.23.154. Before sending anything, the kernel asks one question: is the destination in my own subnet? It checks its routing table:&lt;/p&gt;

&lt;p&gt;default via 10.0.2.2 dev enp0s3 proto dhcp src 10.0.2.15 metric 100&lt;br&gt;
10.0.2.0/24 dev enp0s3 proto kernel scope link src 10.0.2.15 metric 100&lt;/p&gt;

&lt;p&gt;My address is 10.0.2.15/24. The /24 means the first 24 bits are the network, so my subnet is 10.0.2.0 to 10.0.2.255. 104.20.23.154 isn't in it, so the packet goes to the default route: hand it to the gateway at 10.0.2.2 and let the gateway take it onward.&lt;/p&gt;

&lt;p&gt;Notice that the gateway is inside my subnet. That's no accident. A machine can only hand a packet directly to something it can reach directly (it asks for that machine's hardware address with ARP). If the gateway were somewhere else, I'd need another gateway to reach it. This is why the CIDR math matters: that one "in my subnet or not?" question is asked for every packet.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;TCP: three packets before any web traffic&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I captured plain HTTP on port 80 so the request is readable. (Real sites use HTTPS; more on that next.) Trimmed:&lt;/p&gt;

&lt;p&gt;10.0.2.15.33080 &amp;gt; 104.20.23.154.80: Flags [S], seq 246716017, ...&lt;br&gt;
104.20.23.154.80 &amp;gt; 10.0.2.15.33080: Flags [S.], seq 145920001, ack 246716018, ...&lt;br&gt;
10.0.2.15.33080 &amp;gt; 104.20.23.154.80: Flags [.], ack 1, ...&lt;br&gt;
10.0.2.15.33080 &amp;gt; 104.20.23.154.80: Flags [P.], seq 1:75, ack 1, ... length 74: HTTP: GET / HTTP/1.1&lt;br&gt;
104.20.23.154.80 &amp;gt; 10.0.2.15.33080: Flags [P.], seq 1:932, ack 75, ... length 931: HTTP: HTTP/1.1 200 OK&lt;br&gt;
10.0.2.15.33080 &amp;gt; 104.20.23.154.80: Flags [F.], seq 75, ack 937, ...&lt;br&gt;
104.20.23.154.80 &amp;gt; 10.0.2.15.33080: Flags [F.], seq 937, ack 76, ...&lt;br&gt;
[S], [S.], [.] is the three-way handshake: SYN, SYN-ACK, ACK. The client picks a random high port (33080), and the server answers from port 80. The server's ack 246716018 is my sequence number plus one. About 21 ms passed between SYN and SYN-ACK, which is one round trip to the server.&lt;br&gt;
[P.] pushes data. The first one is the 74-byte request, GET / HTTP/1.1. The next is the 931-byte 200 OK response.&lt;br&gt;
[F.] is the teardown. My side sent the first FIN, because curl was done, and the server answered with its own.&lt;/p&gt;

&lt;p&gt;Whichever side closes first ends up holding the connection in a state called TIME-WAIT for about a minute. I'll come back to that.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;HTTP, and what TLS costs&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The request itself is small: a method (GET), a path, some headers, and sometimes a body. The server's answer begins with a status code, its one-word verdict. Everything before this point was plumbing.&lt;/p&gt;

&lt;p&gt;Over HTTPS, the same exchange runs inside an encrypted channel, set up after the TCP handshake and before the request. curl can show where the time goes:&lt;/p&gt;

&lt;p&gt;dns connect tls ttfb total&lt;br&gt;
0.002155 0.023478 0.113068 0.142943 0.143793&lt;/p&gt;

&lt;p&gt;These numbers are cumulative from the start, so you subtract to get each phase:&lt;/p&gt;

&lt;p&gt;Phase   Time&lt;br&gt;
DNS lookup  2 ms (answered from cache)&lt;br&gt;
TCP connect 21 ms (one round trip)&lt;br&gt;
TLS handshake   90 ms&lt;br&gt;
Waiting for the first byte  30 ms&lt;br&gt;
Download    under 1 ms&lt;/p&gt;

&lt;p&gt;TLS took about four times longer than the TCP connect. It needs extra exchanges and real cryptographic work; I didn't measure how the 90 ms splits between the two. The 21 ms matches the SYN to SYN-ACK gap in the capture, which was a useful cross-check between two different tools. On a cold run the DNS step cost 61 ms instead of 2, because the answer had expired and the resolver had to ask upstream.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;What has to be true on the other end&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Something must be listening. On my VM that's a small Python web app, kept alive by a systemd unit:&lt;/p&gt;

&lt;p&gt;[Service]&lt;br&gt;
User=webapp&lt;br&gt;
ExecStart=/opt/webapp/app.py&lt;br&gt;
Restart=on-failure&lt;br&gt;
RestartSec=1&lt;/p&gt;

&lt;p&gt;ss -tlnp shows the listener (python3 on 0.0.0.0:8080), and journalctl -u webapp shows its logs. Restart=on-failure restarts the app after a kill -9 but not after a clean exit.&lt;/p&gt;

&lt;p&gt;TCP's closing rules bit me here. After a hard kill, the app flapped on restart with Address already in use, because the old connection was still sitting in TIME-WAIT and the new process couldn't bind its port. The fix was setting SO_REUSEADDR on the listening socket.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When it breaks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three real symptoms, and what each one taught me:&lt;/p&gt;

&lt;p&gt;The name fails, but the address works. curl &lt;a href="https://example.com" rel="noopener noreferrer"&gt;https://example.com&lt;/a&gt; timed out at "Resolving", yet ping 8.8.8.8 worked fine. The network was fine, and the resolver configuration was the problem. I had replaced /etc/resolv.conf (really a symlink) with a file naming a dead server, and the system's resolver service adopted it. Restoring the symlink wasn't enough: the service kept the bad server until I restarted it.&lt;/p&gt;

&lt;p&gt;A connection hangs at SYN. My study notes said this is "a firewall or a routing problem, never the application." That turned out to be too strong. I started a server that listened with a backlog of 1 and never called accept(). Of four connection attempts, two succeeded and two timed out, exactly like a firewall dropping packets. ss -tln showed the listener with Recv-Q 2 (queued, never accepted) and Send-Q 1 (the backlog). When the application stops accepting, the kernel drops new SYNs.&lt;/p&gt;

&lt;p&gt;A service won't start. systemctl status showed status=203/EXEC and a restart loop. The path in ExecStart pointed at a file that didn't exist. The journal said only 203, never why. Only ls on the path told me.&lt;/p&gt;

&lt;p&gt;The short version&lt;/p&gt;

&lt;p&gt;Name, route, handshake, request. Everything else is a variation on those four. When something breaks now, my first question is which of the four failed, and a one-line test usually answers it: ping an IP against curl a name, dig +trace for the delegation chain, tcpdump for the handshake, ss for what's listening.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>networking</category>
      <category>linux</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
