DEV Community

Cover image for What Actually Happens When You Run `curl https://example.com`
Syed Anzar
Syed Anzar

Posted on

What Actually Happens When You Run `curl https://example.com`

What Actually Happens When You Run curl https://example.com

You type curl https://example.com and hit Enter. HTML appears. You've done this thousands of times. But do you know what actually happened between keystroke and output?

Most developers don't. They know the names — DNS, TCP, TLS, HTTP/2 — but they've never seen the full sequence laid out end to end, with real curl -v output showing exactly what each layer does and when.

Here's the complete picture.

The Four Layers, In Order

Every HTTPS request runs through four independent layers, each doing one job:

DNS → TCP → TLS → HTTP/2
Enter fullscreen mode Exit fullscreen mode

The output of one layer becomes the input of the next. curl -v prints them top to bottom, in that exact order.

A real request to example.com:

$ curl -v https://example.com
* Host example.com:443 was resolved.
* IPv6: 2606:4700:90c5:72db:f264:5bb:ef6b:ff98
* IPv4: 172.66.147.243, 104.20.23.154
*   Trying [2606:4700:90c5:72db:f264:5bb:ef6b:ff98]:443...
* Connected to example.com (2606:4700:90c5:72db:f264:5bb:ef6b:ff98) port 443
* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=example.com
*  start date: Jul 29 22:10:08 2026 GMT
*  expire date: Oct 27 22:17:21 2026 GMT
*  subjectAltName: host "example.com" matched cert's "example.com"
*  issuer: C=US; O=SSL Corporation; CN=Cloudflare TLS Issuing ECC CA 3
*  SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://example.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: example.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.5.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: example.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/2 200
< date: Fri, 04 Sep 2026 13:28:04 GMT
< content-type: text/html
< server: cloudflare
< last-modified: Sun, 30 Aug 2026 04:11:49 GMT
< allow: GET, HEAD
< accept-ranges: bytes
< age: 8141
< cf-cache-status: HIT
< cf-ray: a35d55d42b94efe6-DEL
Enter fullscreen mode Exit fullscreen mode

That's the whole thing. Now let's break down what each section actually means.


Layer 1: DNS — "Was Resolved"

* Host example.com:443 was resolved.
* IPv6: 2606:4700:90c5:72db:f264:5bb:ef6b:ff98
* IPv4: 172.66.147.243, 104.20.23.154
Enter fullscreen mode Exit fullscreen mode

What happens: curl asks the OS resolver for example.com. The resolver checks its cache, then your OS cache, then your configured DNS server (usually your ISP or something like 1.1.1.1/8.8.8.8). That resolver walks the DNS hierarchy: root → .com TLD → example.com authoritative nameservers → IP address.

What developers get wrong: They think DNS is "one lookup." It's a chain of delegated queries. The resolver you hit is just the first hop. Also: curl -v shows the result, not the process. The "was resolved" line appears after the full recursive chain completes.

Happy Eyeballs: Notice both IPv6 and IPv4 addresses returned. curl tries IPv6 first (RFC 8305 "Happy Eyeballs"), falls back to IPv4 if it fails. This happens automatically — you don't configure it.


Layer 2: TCP — The Three-Way Handshake

*   Trying [2606:4700:90c5:72db:f264:5bb:ef6b:ff98]:443...
* Connected to example.com (2606:4700:90c5:72db:f264:5bb:ef6b:ff98) port 443
Enter fullscreen mode Exit fullscreen mode

What happens: curl opens a socket, sends a TCP SYN to port 443. The server replies SYN-ACK. curl sends ACK. Connection established. This is one round trip (1 RTT).

What developers get wrong: They conflate "connection" with "TLS handshake." TCP establishes a reliable byte stream. TLS establishes encryption. Separate layers. You can have TCP without TLS (HTTP), but not HTTPS without TCP.

Connection reuse: On the second request to the same host, you'll see connect: 0.000000s — the TCP connection was kept alive. HTTP/2 multiplexes many streams over one TCP connection, eliminating the 6-connection limit of HTTP/1.1.


Layer 3: TLS 1.3 — One Round Trip to Encryption

* ALPN: curl offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / id-ecPublicKey
* ALPN: server accepted h2
* Server certificate: ...
*  SSL certificate verify ok.
Enter fullscreen mode Exit fullscreen mode

What happens (TLS 1.3 = 1 RTT):

  1. ClientHello — curl sends its supported cipher suites, key share (X25519), and ALPN protocols (h2, http/1.1)
  2. ServerHello — server picks cipher (TLS_AES_256_GCM_SHA384), sends its key share, selects h2 via ALPN
  3. Encrypted Extensions — server sends extensions that don't affect crypto (like server_name, max_fragment_length)
  4. Certificate — server sends its cert chain (leaf → intermediate → root)
  5. CertificateVerify — server proves it owns the private key (signature over handshake transcript)
  6. Finished — server sends MAC over entire handshake, confirming integrity
  7. Client Finished — curl verifies everything, sends its own Finished

After this, everything is encrypted. The HTTP request/response you see next? They're inside TLS records. A packet capture shows only TLS application data frames — no HTTP visible.

What developers get wrong:

  • "TLS is part of TCP" — No. TLS runs on top of TCP. Different layers.
  • "The certificate contains the private key" — Never. The cert has the public key. The private key never leaves the server.
  • "ALPN negotiates HTTP/2" — ALPN selects the application protocol during the TLS handshake, before any HTTP bytes are sent. That's why h2 appears in the ClientHello.
  • "Change Cipher Spec means something in TLS 1.3" — It doesn't. It's a compatibility artifact for middleboxes that expect it from TLS 1.2. Ignore it.

Session tickets: At the end, the server sends NewSessionTicket messages. These let the client resume the session on the next connection with 0-RTT — sending application data with the ClientHello, skipping the handshake entirely. That's why the second request shows tls: 0.000000s.


Layer 4: HTTP/2 — Binary Frames, Multiplexed Streams

* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://example.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: example.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.5.0]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: example.com
> User-Agent: curl/8.5.0
> Accept: */*
>
< HTTP/2 200
< date: Fri, 04 Sep 2026 13:28:04 GMT
< content-type: text/html
...
Enter fullscreen mode Exit fullscreen mode

What happens: HTTP/2 is a binary framing layer over TLS. The client sends a 24-byte connection preface (PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n), then a SETTINGS frame. The server replies with its SETTINGS. Then both sides exchange frames on streams (identified by stream ID).

Key frames you're seeing:

  • HEADERS frame — carries pseudo-headers (:method, :path, :authority, :scheme) + regular headers
  • DATA frame — carries request/response body
  • SETTINGS — connection configuration (max frame size, header table size, etc.)
  • WINDOW_UPDATE — flow control (each stream + connection has a receive window)

Multiplexing: Multiple requests/responses interleave on the same TCP connection. Stream 1, Stream 3, Stream 5 — frames from all of them mixed together. The receiver reassembles by stream ID. No head-of-line blocking at the HTTP layer (though TCP head-of-line blocking still exists — that's what HTTP/3/QUIC solves).

What developers get wrong:

  • "HTTP/2 is just HTTP/1.1 with compression" — No. It's a completely different wire format. Binary frames, not text lines.
  • "Header compression = gzip" — No. HPACK (RFC 7541) is a stateful compression: both ends maintain a dynamic header table. Repeated headers (like user-agent) become tiny references.
  • "The > and < lines in curl -v are HTTP/1.1 format" — curl displays them that way for readability. On the wire, they're binary HEADERS frames.

The Timing Breakdown (What curl -w Shows)

$ curl -w "dns: %{time_namelookup}s\nconnect: %{time_connect}s\ntls: %{time_appconnect}s\npretransfer: %{time_pretransfer}s\nttfb: %{time_starttransfer}s\ntotal: %{time_total}s\n" -o /dev/null -s https://example.com

dns: 0.191242s
connect: 0.414582s
tls: 1.098157s
pretransfer: 1.098352s
ttfb: 1.726874s
total: 1.726993s
Enter fullscreen mode Exit fullscreen mode

Read as deltas (this is how you actually debug):

Phase Calculation Time What It Means
DNS time_namelookup 191ms Resolver chain latency
TCP time_connect - time_namelookup 223ms 1 RTT to server
TLS time_appconnect - time_connect 684ms TLS 1.3 handshake (1 RTT + crypto)
Server time_starttransfer - time_appconnect 629ms Server processing + network return
Transfer time_total - time_starttransfer ~0ms Body download (tiny response)

Second request (connection reused + session resumption):

dns: 0.000028s
connect: 0.000000s
tls: 0.000000s
pretransfer: 0.000079s
ttfb: 0.091438s
total: 0.091513s
Enter fullscreen mode Exit fullscreen mode

Zero DNS, zero TCP, zero TLS — the connection was alive and the TLS session resumed via 0-RTT ticket. Only TTFB remains (91ms = server processing + 1 RTT).


Common Misconceptions, Debunked

Misconception Reality
"DNS resolves to one IP" Returns multiple (IPv4 + IPv6). Client picks via Happy Eyeballs.
"TLS handshake is 2 round trips" TLS 1.2 was 2 RTT. TLS 1.3 is 1 RTT (or 0-RTT on resume).
"HTTP/2 requires HTTPS" Technically h2c (cleartext HTTP/2) exists via Upgrade, but no browser supports it. In practice: yes, HTTPS only.
"ALPN is optional" For HTTPS, mandatory. Without ALPN, server doesn't know to speak HTTP/2.
"Certificate verification happens after handshake" It's part of the handshake. CertificateVerify proves possession of private key.
"The response headers are text" On the wire: binary HEADERS frames with HPACK encoding.

Mental Model Checklist

Next time you debug a slow or failing request, map it to the layer:

  1. DNS slow?dig +trace example.com, check TTL, try different resolver
  2. TCP slow? → Check RTT (ping), firewall drops (SYN retransmits in capture)
  3. TLS slow?openssl s_client -connect example.com:443 -tls1_3, verify TLS 1.3 enabled, check session resumption
  4. TTFB slow?curl -I to check Cache-Control, profile origin with OpenTelemetry
  5. Total slow but TTFB fast? → Large body, enable Brotli/gzip, check Content-Length vs Transfer-Encoding: chunked

Why This Matters

You use curl daily. Your apps make HTTPS requests constantly. But the mental model most developers carry is fuzzy: "DNS → magic → response."

When production latency spikes, the difference between "it's the network" and "it's the database" is knowing which layer the time lives in. curl -w gives you that breakdown in one command. Packet captures (tcpdump -i eth0 -w trace.pcap host example.com and port 443) let you verify each layer on the wire.

The stack hasn't changed in decades. DNS → TCP → TLS → HTTP. What changes is which layer is the bottleneck today.

Now you can point at the exact line in curl -v output and say: "This is where the time goes."


TL;DR

  • DNS resolves the name → IP (recursive chain, cached at multiple levels)
  • TCP 3-way handshake → reliable byte stream (1 RTT, reusable via keep-alive)
  • TLS 1.3 1-RTT handshake → encrypted channel + ALPN selects HTTP/2 (resumable via 0-RTT tickets)
  • HTTP/2 binary frames on multiplexed streams → request/response inside TLS records

Run curl -v and curl -w on your own endpoints. Map the output to the layers. That's the mental model that turns "it's slow" into "TLS handshake is 400ms because session resumption isn't working."

Top comments (0)