Feb 18, 2026

Life of a cURL request

If you work with HTTP services, then you probably use cURL quite often for API testing. That’s usually enough for most developers as it’s simple and gets the job done. However, as I used cURL more and more for troubleshooting as opposed for simply testing, I realized there’s a lot more you can get out of it when you know what to look for.

The Request

To understand what cURL does under the hood, we can use the -v or --verbose flag. For this post, we’ll simply be using cURL on my blog site https://dev.chevonair.com.

$ curl -v https://dev.chevonair.com/
* Host dev.chevonair.com:443 was resolved.
* IPv6: (none)
* IPv4: 104.21.42.143, 172.67.162.130
*   Trying 104.21.42.143:443...
* Connected to dev.chevonair.com (104.21.42.143) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=dev.chevonair.com
*  start date: Feb  3 20:18:21 2026 GMT
*  expire date: May  4 21:18:13 2026 GMT
*  subjectAltName: host "dev.chevonair.com" matched cert's "dev.chevonair.com"
*  issuer: C=US; O=Google Trust Services; CN=WE1
* SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://dev.chevonair.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: dev.chevonair.com]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: dev.chevonair.com
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 200
< date: Wed, 18 Feb 2026 15:15:55 GMT
< content-type: text/html; charset=utf-8
< access-control-allow-origin: *
< cache-control: public, max-age=0, must-revalidate
< referrer-policy: strict-origin-when-cross-origin
< x-content-type-options: nosniff
< report-to: {"group":"cf-nel","max_age":604800,"endpoints":[{"url":"https://a.nel.cloudflare.com/report/v4?s=%2BQxtYkzililASu8oZ22BfLmpIVTmDvM7n8f4adPAiIj69IBNsLfweruEOW6%2FKTFRBRJ7M7KE61XDNmkCQwUFcR41bOylKRcuGqHn7CSvukaSAt"}]}
< nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
< server: cloudflare
< cf-cache-status: DYNAMIC
< cf-ray: 9cfe7b89a8dbfd35-SIN
< alt-svc: h3=":443"; ma=86400
<
<!DOCTYPE html><html lang="en"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width"><link rel="icon" type="image/svg+xml" href="/favicon.svg"><meta name="generator" content="Astro v5.9.2"><title>Chevonair</title><link rel="stylesheet" href="/_astro/index.B1Ac6BUf.css"><script type="module" src="/_astro/page.V2R8AmkL.js"></script></head> <body class="h-full bg-white p-4 md:p-10"> <main class="mx-auto max-w-[920px] px-2 md:px-4 py-10"> <header class="flex flex-col-reverse md:flex-row gap-6 border-b border-red-800 border-spacing-lg pb-6"> <a href="https://github.com/nayyara-airlangga" target="_blank" rel="noopener noreferrer" class="relative mb-4 md:mb-0 mx-auto size-36 border-2 border-red-800 rounded-full"> <img src="/_astro/profile.0I_XdQlT.png" alt="@nayyara-airlangga" class="w-full h-full object-cover rounded-full"> </a> <div class="flex-1"> <h1 class="text-xl md:text-2xl font-semibold"> <span class="text-red-800">Chevonair</span> <span class="text-black">/</span> <span class="text-reddish-grey-400">Nayyara</span> </h1> <p class="text-sm md:text-base leading-relaxed text-red-950/80 py-2 max-w-[60ch] font-serif">
A few things that I am:
</p> <ul class="text-sm md:text-base leading-relaxed text-red-950/80 py-2 max-w-[60ch] list-disc list-inside font-serif"> <li class="text-switcher overflow-hidden h-8 relative font-sans"> <span class="text-white switching-text font-semibold absolute top-0 md:left-[21.5px] left-[18.5px] max-w-fit px-2 transition-transform duration-500 ease-out bg-red-800 rounded-xl">Software Engineer</span> </li> <li>Single Player RPG and Game Music Enjoyer</li> </ul> <p class="text-sm md:text-base leading-relaxed text-red-950/80 py-2 max-w-[80ch] font-serif">
I love building scalable systems that solve real
                        problems and keep teams moving fast. Aside from the
                        handles above, some may know me as <u>Angga</u>.
</p> <div class="flex items-center gap-2 pt-2"></div> </div> </header> <section id="blog" class="pt-6 md:pt-10 pb-6"> <h2 class="text-3xl md:text-4xl font-semibold"> <a data-astro-prefetch href="/blog" class="text-red-950/80 hover:text-red-500">Blog</a> </h2> <p class="text-red-950/80 pt-2 pb-6 font-serif">
Sometimes I post interesting things about my work. Although,
                    I'll probably dump other things too here...
</p> <ul class="flex flex-col gap-4"> <li> <article> <p class="text-reddish-grey-500 text-sm md:text-base"> May 12, 2027 </p> <h3 class="text-xl md:text-2xl font-semibold"> <a href="/blog/hrmmm" class="text-red-800 hover:text-red-500"> Hrmmmm </a> </h3> <p class="text-red-950/80 pt-2 font-serif"> hmm </p> </article> </li><li> <article> <p class="text-reddish-grey-500 text-sm md:text-base"> Jun 11, 2025 </p> <h3 class="text-xl md:text-2xl font-semibold"> <a href="/blog/simple-secure-cloudsql-connection" class="text-red-800 hover:text-red-500"> Simple and secure Cloud SQL connections over public IP </a> </h3> <p class="text-red-950/80 pt-2 font-serif"> Cloud SQL is GCP&#39;s managed relational database service for Postgres, MySQL, and SQL Server. It&#39;s a great service for teams, businesses, or companies that don&#39;t have the operational... </p> </article> </li> </ul>  </section> <section id="played-list" class="pt-6 md:pt-10 pb-6"> <h2 class="text-3xl md:text-4xl font-semibold"> <a data-astro-prefetch href="/playedlist" class="text-red-950/80 hover:text-red-500">Playedlist</a> </h2> <p class="text-red-950/80 pt-2 pb-6 font-serif">
A list of my favorite games
</p> <ul class="grid md:grid-cols-3 sm:grid-cols-2 grid-cols-1 gap-4"> <li> <article class="border-6 rounded-lg border-red-700 border-double font-opendyslexic"> <a href="/playedlist/nine-sols"> <img src="https://shared.fastly.steamstatic.com/store_item_assets/steam/apps/1809540/header.jpg" alt="A cover image of Nine Sols" class="w-full"> </a> <div class="flex flex-row justify-between p-2 items-center"> <h3 class="text-lg font-semibold"> <a href="/playedlist/nine-sols" class="text-red-800 hover:text-red-500"> Nine Sols </a> </h3> <div class="flex flex-row gap-x-2 items-center"> <a target="_blank" rel="noopener" href="https://store.steampowered.com/app/1809540/Nine_Sols/" class="hover:text-red-600 duration-200"> <span class="icon-[simple-icons--steam] w-6 h-6"></span> </a> </div> </div> <ul class="flex flex-wrap flex-row gap-2 px-2 pb-3"> <li class="italic text-xs bg-reddish-grey-500 px-2 py-1 rounded-lg text-white"> Metroidvania </li><li class="italic text-xs bg-reddish-grey-500 px-2 py-1 rounded-lg text-white"> Souls-like </li><li class="italic text-xs bg-reddish-grey-500 px-2 py-1 rounded-lg text-white"> Difficult </li><li class="italic text-xs bg-reddish-grey-500 px-2 py-1 rounded-lg text-white"> Action </li><li class="italic text-xs bg-reddish-grey-500 px-2 py-1 rounded-lg text-white"> 2D </li> </ul> </article> </li> </ul>  </section> </main> <script type="module">const n=document.querySelector(".text-switcher"),s=["Software Engineer","Site Reliability Engineer"];let o=0;function i(){if(n!==null){const e=n.querySelector(".switching-text"),r=(o+1)%s.length,t=document.createElement("span");t.className="text-white switching-text font-semibold absolute top-0 md:left-[21.5px] left-[18.5px] transition-transform duration-500 ease-out bg-red-800 rounded-xl max-w-fit px-2",t.textContent=s[r],t.style.transform="translateY(-100%)",n.appendChild(t),t.offsetHeight,e!==null&&(requestAnimationFrame(()=>{e.style.transform="translateY(125%)",t.style.transform="translateY(0)"}),setTimeout(()=>{e.remove()},500),o=r)}}setInterval(i,3500);</script> </body></html>%
* Connection #0 to host dev.chevonair.com left intact

DNS Resolution

Before any network traffic can flow, cURL does a DNS lookup for dev.chevonair.com to get its corresponding IP addresses.

* Host dev.chevonair.com:443 was resolved.
* IPv6: (none)
* IPv4: 104.21.42.143, 172.67.162.130

The resolver first checks its local cache and /etc/hosts before querying upstream DNS servers. In this case, two IPv4 addresses were returned:

  1. 104.21.42.143
  2. 172.67.162.130

These are Cloudlare IP addresses. What this tells us is that Cloudflare’s CDN is serving this request from multiple anycast edge locations. The lack of IPv6 (AAAA records) suggests either the domain isn’t configured for IPv6, or the resolver couldn’t reach the authoritative nameservers for AAAA records.

Modern clients typically perform DNS resolution in parallel for A and AAAA records to minimize latency. The resolver may return multiple IPs for load balancing or failover; cURL will try them in order until one connects successfully.

TCP Connection Establishment

Once you have an IP address, the client initiates a TCP connection. TCP provides reliable, ordered, error-checked delivery through a three-way handshake: the client sends a SYN packet, the server responds with SYN-ACK, and the client completes with ACK. This exchange ensures both sides are ready to receive data before any application traffic flows.

*   Trying 104.21.42.143:443...
* Connected to dev.chevonair.com (104.21.42.143) port 443

The Trying line shows cURL attempting to connect to port 443 (the standard HTTPS port). The handshake completes when you see “Connected”—at this point, you have a raw TCP socket, but the connection is unencrypted. The client and server have agreed on initial sequence numbers and can now exchange segments reliably.

There are some indicators that may be of interest when you are troubleshooting:

  1. If the connection hangs after “Trying”, you could be dealing with a network that can’t be reached, a firewall silently dropping packets, or a server not responding on that IP and port.
  2. A connection refused error means nothing is actively listening on that port — usually a misconfigured port number or a service that’s down.
  3. If cURL falls through to a second IP after the first one fails, that might be worth investigating. It means one of the IPs being advertised for that hostname isn’t reachable, which could indicate a partially degraded infrastructure or a misconfigured origin behind the load balancer.
    * IPv4: 10.3.3.228, 10.3.2.49, 10.3.20.85
    *   Trying 10.3.3.228:443...
    *   Trying 10.3.2.49:443...

These commands may help you when encountering the issues above:

  1. nc -zv 104.21.42.143 443 to verify port accessibility
  2. tcptraceroute 104.21.42.143 443 to identify network path problems

TLS Handshake

With TCP established, the client and server perform a TLS handshake to establish an encrypted channel. TLS (Transport Layer Security) provides three properties: confidentiality (encryption), integrity (tamper detection), and authentication (proof of identity via certificates). The handshake negotiates cryptographic algorithms, verifies the server’s certificate, and establishes session keys.

ALPN: Choosing the Application Protocol

* ALPN: curl offers h2,http/1.1

ALPN (Application-Layer Protocol Negotiation) is a TLS extension that lets client and server agree on which application protocol to use—HTTP/1.1, HTTP/2, or HTTP/3—during the TLS handshake. This avoids extra round trips that would be needed if protocol negotiation happened at the application layer (e.g. via an Upgrade header for upgrading from HTTP/1.1 to HTTP/2). As seen above, cURL advertises support for both h2 (HTTP/2) and http/1.1; the server selects h2 as seen in the later output.

* ALPN: server accepted h2

Walking Through the Handshake

* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: /etc/ssl/cert.pem
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256

The numbered messages (1, 2, 8, 11, 15, 20) correspond to TLS handshake message types defined in RFC 8446, which describes the specifications for TLS 1.3. The IN and OUT defines the direction of traffic (IN being from server to client and OUT for client to server).

TLS 1.3 vs TLS 1.2

One of the more meaningful improvements TLS 1.3 brought over TLS 1.2 is a reduction in handshake round trips from 2-RTT down to 1-RTT. In TLS 1.2, the first round trip handles the ClientHello, ServerHello, Certificate, and KeyExchange, and only after a second round trip for the Finished messages can application data actually start flowing. TLS 1.3 collapses this by having the ClientHello carry a key share upfront, allowing the ServerHello to respond with its own key share and Finished in a single round trip.

Decoding the Cipher Suite

* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256

The cipher suite AEAD-CHACHA20-POLY1305-SHA256 specifies four components:

Certificate Verification

* Server certificate:
*  subject: CN=dev.chevonair.com
*  start date: Feb  3 20:18:21 2026 GMT
*  expire date: May  4 21:18:13 2026 GMT
*  subjectAltName: host "dev.chevonair.com" matched cert's "dev.chevonair.com"
*  issuer: C=US; O=Google Trust Services; CN=WE1
* SSL certificate verify ok.

The server presents a certificate issued by Google Trust Services (CN=WE1) for dev.chevonair.com. cURL verified the certificate chain against its trust store, confirmed the certificate isn’t expired, and validated that the hostname matches the Subject Alternative Name (SAN) or Common Name (CN).

A few things worth paying attention to when inspecting TLS output:

  1. ALPN tells you which HTTP version what the client offered versus what the server accepted. A mismatch here means no common protocol was found and the connection will fail.
  2. TLS version determines which cipher suites are available and directly affects handshake latency. TLS 1.3 costs one round trip, TLS 1.2 costs two.
  3. Missing intermediate certificates in a certificate chain are a common source of verification failures, even when the end certificate itself is valid.
  4. Expired or not-yet-valid certificates fail immediately. It is worth checking both the start date and expire date fields explicitly.
  5. Modern clients validate the hostname against the Subject Alternative Name extension, not the CN field. A cert without a matching SAN will fail even if the CN looks correct.

These commands and indicators could help you troubleshoot the issues mentioned above:

  1. openssl s_client -connect dev.chevonair.com:443 -tls1_3tests connectivity against a specific TLS version.
  2. openssl s_client -connect dev.chevonair.com:443 -showcerts dumps the full certificate chain so you can spot missing intermediates.
  3. A certificate verify failed error in cURL points to either a chain validation issue or a hostname mismatch.
  4. no ciphers shared means the client and server have no overlapping cipher suites.
  5. ALPN protocol mismatch means no common HTTP version was agreed upon.

HTTP/2 Connection

With encryption established, the client and server speak HTTP/2 as agreed upon via ALPN.

* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://dev.chevonair.com/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: dev.chevonair.com]
* [HTTP/2] [1] [:path: /]

HTTP/2 organizes data into streams—bidirectional sequences of frames exchanged between client and server. Stream 1 is opened for the initial request to /. The :method, :scheme, :authority, and :path are pseudo-headers (prefixed with :) that appear in the HEADERS frame instead of HTTP/1-style request lines.

HTTP/2 uses HPACK for header compression, maintaining dynamic and static tables to eliminate redundant header transmission on both client and servers. These means that subsequent requests to the same origin can reference previously seen headers, which significantly reduces the bandwidth for cookie-heavy or repetitive header patterns (this information is more useful for clients with persistence in their behavior such as browsers and mobile devices as opposed to cURL but it’s still worth noting)

HTTP/2’s multiplexing allows multiple requests to be in flight simultaneously over a single TCP connection as a single TCP connection may have multiple streams with in-flight messages in parallel by allowing the messages for any stream to be interleaved and reassembled at the destination.

These commands and indicators could help when troubleshooting HTTP/2 related issues:

  1. curl -v --http2-prior-knowledge https://dev.chevonair.com forces HTTP/2 without using ALPN or HTTP/1.1 Upgrade headers. Useful for isolating whether the issue is with the protocol itself or the negotiation.
  2. An RST_STREAM frame mid-connection means either the client or server aborted a specific stream. It is worth checking the error code it carries for additional context. The complete list of error codes can be seen in RFC 7540.

Request and Response Exchange

Now the actual HTTP transaction occurs. For our cURL sample, the client sends a GET request for /, and the server responds with a 200 status code and HTML content.

> GET / HTTP/2
> Host: dev.chevonair.com
> User-Agent: curl/8.7.1
> Accept: */*
< HTTP/2 200
< date: Wed, 18 Feb 2026 15:15:55 GMT
< content-type: text/html; charset=utf-8
< access-control-allow-origin: *
< cache-control: public, max-age=0, must-revalidate
< referrer-policy: strict-origin-when-cross-origin
< x-content-type-options: nosniff
< report-to: {"group":"cf-nel","max_age":604800,"endpoints":[...]}
< nel: {"report_to":"cf-nel","success_fraction":0.0,"max_age":604800}
< server: cloudflare
< cf-cache-status: DYNAMIC
< cf-ray: 9cfe7b89a8dbfd35-SIN
< alt-svc: h3=":443"; ma=86400

Response headers can reveal information about not just the application’s response, but also the infrastructure serving it (e.g. Cloudflare for my website).

  1. server: cloudflare indicates that the site is behind Cloudflare’s CDN and proxy layer
  2. cf-ray: 9cfe7b89a8dbfd35-SIN is a unique identifier for a request going through Cloudflare’s network. The SIN suffix indicates that the request was handled by a Cloudflare edge server in Singapore. Providing this ray in support tickets helps trace the request path
  3. cf-cache-status: DYNAMIC indicates response wasn’t served from Cloudflare’s cache but from the origin
  4. alt-svc: h3=":443"; ma=86400 advertises that the server supports HTTP/3 on port 443, and that this advertisement is valid for 24 hours. A compatible client may upgrade to HTTP/3 on its next connection.

Some things worth noting:

  1. Different kinds of status codes mean different things: 2xx means success, 3xx is a redirect, 4xx is a client error, 5xx is a server-side problem. Understanding the status code can help you identify where and what the problem is.
  2. The server header provides information on whether the request hit nginx, Apache, an AWS load balancer, or a CDN like Cloudflare. Knowing the response headers these platforms provide will help you understand the behavior of a request going through the infrastructure.
  3. Cache headers tell you whether content was served from cache or fetched from origin. This is relevant when debugging stale responses or unexpected cache misses. CDNs often have their own specific cache headers which is worth understanding on top of the standard cache headers for HTTP.

Connection Teardown and Reuse

Finally, cURL reports the connection state:

* Connection #0 to host dev.chevonair.com left intact

This indicates that the connection was not closed and is available for potential reuse. HTTP/1.1 uses Connection: keep-alive by default for this whereas HTTP/2 multiplexes all requests over a single connection. Keeping connections open saves significant latency by avoiding:

  1. TCP handshake (1-RTT)
  2. TLS handshake (1-RTT for TLS 1.3, 2-RTT for TLS 1.2)
  3. HTTP/2 stream setup

For a single request to an already warm cache, reusing connections alone save 2-3 RTTs which can be around hundreds of milliseconds on high-latency connections. Keep-alive timeouts are usually tuned at 30-120 seconds for production services to balance connection reuse against resource consumption.

Know Your Tools

There’s nothing wrong with using cURL as a simple HTTP testing tool by firing off a request, checking the response, and moving on. It’s fast, it’s convenient, and most of the time that’s all you need. But when something breaks (and it will eventually), cURL gives you almost the entire story of an HTTP request to help you figure out why. You just have to know what you’re looking at.