> ## Content Index
> Fetch the complete content index at: https://www.pinglabz.com/llms.txt
> Use this file to discover other available public pages before exploring further.

# curl for Network Engineers: Ports, HTTP, APIs
- URL: https://www.pinglabz.com/curl-network-engineers/
- Published: 2026-08-19T15:38:51.000Z
- Updated: 2026-08-19T15:38:51.000Z
- Description: Prove a port is open, split DNS from TCP from server think time, test a device API and read a TLS failure, all with real output from a routed lab.
- Author: Jaime
- Tags: Linux, HTTP, Troubleshooting, Tools, Labs

Ping tells you a host answers ICMP. That is roughly ten percent of what you need to know. The ticket usually says "the application is down", and the application is HTTP, so the question is whether a TCP connection completes, whether the service on the far end responds, how long each stage takes and what the response actually says. `curl` answers all four in one command, and it is on almost every Linux box you will ever be handed.

This guide is written for people who use `curl` as a network test tool rather than as a download client. Every capture below is real output from a Debian 13 host wired into a Cisco Modeling Labs topology, hitting an nginx server two hops away and a small JSON API three hops away across IOS XE routers running OSPF. It is part of the [Linux networking commands](https://www.pinglabz.com/linux-networking-commands/) cluster.

## The lab behind every capture

The client is a Debian 13 host at 10.77.0.100\. WEB1 is an nginx container at 10.77.2.10, two router hops away. SRV1 at 10.77.3.10 is three hops away and runs a static site on 8000, a small JSON API on 8001, and a TLS listener with a self-signed certificate on 8443\. Nothing caches anything in between, so the timing numbers are honest:

```
j@llmbits:~$ traceroute -n 10.77.2.10
traceroute to 10.77.2.10 (10.77.2.10), 30 hops max, 60 byte packets
 1  10.77.0.1  5.226 ms  5.500 ms  5.491 ms
 2  10.77.12.2  6.268 ms  6.234 ms  6.735 ms
 3  10.77.2.10  6.874 ms  6.902 ms  6.920 ms
```

Start by confirming what your `curl` can even do, because the protocol and feature list varies wildly between distributions and container images:

```
j@llmbits:~$ curl --version
curl 8.14.1 (x86_64-pc-linux-gnu) libcurl/8.14.1 OpenSSL/3.5.6 zlib/1.3.1 brotli/1.1.0 zstd/1.5.7 libidn2/2.3.8 libpsl/0.21.2 libssh2/1.11.1 nghttp2/1.64.0 nghttp3/1.8.0 librtmp/2.3 OpenLDAP/2.6.10
Release-Date: 2025-06-04, security patched: 8.14.1-2+deb13u4
Protocols: dict file ftp ftps gopher gophers http https imap imaps ipfs ipns ldap ldaps mqtt pop3 pop3s rtmp rtsp scp sftp smb smbs smtp smtps telnet tftp ws wss
Features: alt-svc AsynchDNS brotli GSS-API HSTS HTTP2 HTTP3 HTTPS-proxy IDN IPv6 Kerberos Largefile libz NTLM PSL SPNEGO SSL threadsafe TLS-SRP UnixSockets zstd
```

That `Protocols:` line matters more than it looks. `curl` speaks SCP, SFTP, TFTP and telnet, which means it can often replace three other tools you were about to install on a locked-down jump box.

## The four commands that cover most tickets

Before the detail, here is the short list. If you learn nothing else from this page, learn these.

**`curl -I http://host/`**Headers only. Proves the TCP connection completes and the service replies, without pulling a megabyte of HTML into your terminal. 

**`curl -v -o /dev/null URL`**The whole conversation, request and response, with the body thrown away. This is the one to paste into a ticket. 

**`curl -o /dev/null -sS -w ...`**The timing breakdown. Splits DNS from TCP from TLS from the server's own think time, which is how you stop guessing whose problem it is. 

**`curl --resolve host:port:IP`**Test one specific server behind a load balancer or a DNS record you are about to change, with the correct `Host:` header and SNI. 

## Is the port open, and is anything listening

The fastest health check is a single line that prints nothing but numbers. `-s` silences the progress meter, `-S` keeps errors visible, `-o /dev/null` discards the body, and `-w` formats exactly the fields you want:

```
j@llmbits:~$ curl -sS -o /dev/null -w 'connect=%{time_connect}s http=%{http_code}\n' http://10.77.2.10/
connect=0.004175s http=200
```

Now compare the three ways a check can fail. They look similar in a monitoring dashboard and they mean completely different things:

```
j@llmbits:~$ curl -sS --connect-timeout 3 http://10.77.2.10:8080/
curl: (7) Failed to connect to 10.77.2.10 port 8080 after 4 ms: Could not connect to server

j@llmbits:~$ curl -sS --connect-timeout 3 http://10.77.9.9/
curl: (7) Failed to connect to 10.77.9.9 port 80 after 3 ms: Could not connect to server

j@llmbits:~$ curl -sS -m 3 http://172.31.99.99/
curl: (28) Connection timed out after 3002 milliseconds
```

The first two came back in single-digit milliseconds, which tells you the network delivered an answer: a TCP reset in the first case, an ICMP unreachable in the second. The third burned the full three seconds with no reply at all, which is what a silently dropping firewall or a black-holed route looks like. Fast failure is a routed network doing its job. Slow failure is something eating your packets.

Exit code 7 is "could not connect" and exit code 28 is "timed out". Both are worth checking for by number in a script rather than grepping the message text, which changes between versions.

## The whole conversation, with -v

When a request behaves strangely, stop reading the body and read the exchange. Lines starting with `>` are what you sent, `<` is what came back, and `*` is `curl` narrating:

```
j@llmbits:~$ curl -v -o /dev/null http://10.77.2.10/
*   Trying 10.77.2.10:80...
* Connected to 10.77.2.10 (10.77.2.10) port 80
* using HTTP/1.x
> GET / HTTP/1.1
> Host: 10.77.2.10
> User-Agent: curl/8.14.1
> Accept: */*
>
* Request completely sent off
< HTTP/1.1 200 OK
< Server: nginx/1.29.8
< Date: Wed, 19 Aug 2026 15:01:26 GMT
< Content-Type: text/html
< Content-Length: 896
< Last-Modified: Tue, 07 Apr 2026 11:37:12 GMT
< Connection: keep-alive
< ETag: "69d4ec68-380"
< Accept-Ranges: bytes
<
{ [896 bytes data]
* Connection #0 to host 10.77.2.10 left intact
```

The `Host:` header is the field that catches people out. A virtual-hosted server picks its site from that header, not from the IP you connected to, so hitting a server by address and getting the wrong site is expected behavior rather than a fault.

If you only want the response headers and no narration, `-D -` dumps them to stdout while `-o /dev/null` throws the body away:

```
j@llmbits:~$ curl -sS -o /dev/null -D - -H 'X-Lab: pinglabz' http://10.77.2.10/
HTTP/1.1 200 OK
Server: nginx/1.29.8
Date: Wed, 19 Aug 2026 15:01:47 GMT
Content-Type: text/html
Content-Length: 896
Last-Modified: Tue, 07 Apr 2026 11:37:12 GMT
Connection: keep-alive
ETag: "69d4ec68-380"
Accept-Ranges: bytes
```

## Timing breaks the argument

"The app is slow" is not a network fault until you show which stage is slow. The `-w` variables give you a stage-by-stage breakdown, all cumulative from the start of the request:

```
j@llmbits:~$ curl -o /dev/null -sS -w 'dns=%{time_namelookup} connect=%{time_connect} start=%{time_starttransfer} total=%{time_total} size=%{size_download}\n' http://10.77.2.10/
dns=0.000049 connect=0.004498 start=0.008633 total=0.009151 size=896
```

Read the gaps rather than the absolute numbers:

**`time_namelookup`**DNS. Large here and small everywhere else means a resolver problem, not a network problem. Zero when you used a literal IP, as above. 

**`time_connect` minus DNS**The TCP handshake, which is one round trip. This is your real path latency, and it should track your ping RTT closely. 

**`time_appconnect`**TLS handshake completion. The gap from `time_connect` is crypto and certificate work, typically one or two extra round trips. 

**`time_starttransfer`**First byte of the body. The gap from connect or appconnect is the server thinking. A big gap here is an application or database problem and nothing to do with you. 

**`time_total`**Everything. The gap from starttransfer is the body transfer, so a big gap with a big `size_download` is a throughput question. Take that to [iperf3](https://www.pinglabz.com/iperf/), not to curl. 

Keep that `-w` string in a file and reference it with `-w @curl-format.txt` so you are not retyping it at three in the morning.

## Testing one specific server behind a name

`--resolve` overrides DNS for a single hostname and port, so the request carries the correct `Host:` header and, over TLS, the correct SNI, while landing on the machine you choose. It is the right way to test one node behind a load balancer, or to validate a DNS change before you make it:

```
j@llmbits:~$ curl -sS -I --resolve www.pinglabz.lab:80:10.77.2.10 http://www.pinglabz.lab/
HTTP/1.1 200 OK
Server: nginx/1.29.8
Date: Wed, 19 Aug 2026 15:01:27 GMT
Content-Type: text/html
Content-Length: 896
Last-Modified: Tue, 07 Apr 2026 11:37:12 GMT
Connection: keep-alive
ETag: "69d4ec68-380"
Accept-Ranges: bytes
```

People reach for `-H 'Host: www.example.com'` instead. That works over plain HTTP and breaks over HTTPS, because it fixes the header but not the SNI in the TLS handshake, so the server presents the wrong certificate. Use `--resolve`.

## Talking to a device API

Modern network gear exposes RESTCONF or a vendor REST API, and `curl` is the fastest way to find out whether the endpoint is up, whether your credentials work and what shape the data is, before you write a line of Python. Here is the lab's small JSON API three hops away:

```
j@llmbits:~$ curl -sS http://10.77.3.10:8001/api/devices
{
  "device": "SRV1",
  "os": "net-tools",
  "interfaces": [
    {
      "name": "eth0",
      "ipv4": "10.77.3.10/24",
      "state": "up"
    }
  ],
  "uptime_seconds": 0
}
```

Sending JSON is `-X POST`, a content type header and `-d` with the body. Note that the API echoed back exactly what it parsed, which is how you confirm your quoting survived the shell:

```
j@llmbits:~$ curl -sS -X POST http://10.77.3.10:8001/api/devices -H 'Content-Type: application/json' -d '{"hostname":"R4","loopback":"4.4.4.4"}'
{
  "created": true,
  "received": {
    "hostname": "R4",
    "loopback": "4.4.4.4"
  }
}
```

And a deliberately malformed body, because you will hit this and it is worth recognizing the shape of the error:

```
j@llmbits:~$ curl -sS -X POST http://10.77.3.10:8001/api/devices -H 'Content-Type: application/json' -d '{bad json}'
{
  "error": "invalid json"
}
```

Two habits that save time on real APIs. Put the body in a file and use `-d @payload.json` so the shell stops mangling your quotes. And append `-w '\nHTTP %{http_code}\n'`, because a REST API that returns 401 or 404 with a perfectly valid JSON body will otherwise look like success:

```
j@llmbits:~$ curl -sS -w '\nHTTP %{http_code}\n' http://10.77.3.10:8001/api/nope
{
  "error": "not found",
  "path": "/api/nope"
}
HTTP 404
```

For RESTCONF on IOS XE specifically, add `-k` (the box ships a self-signed certificate), `-u user:pass`, and `-H 'Accept: application/yang-data+json'`. Once `curl` proves the endpoint works, move to the libraries covered in the [network automation](https://www.pinglabz.com/network-automation/) cluster.

## TLS problems, and how to read them

Half of "it works from my laptop but not from the server" is certificate trust. `curl` is strict by default, which is correct and also the first thing everybody disables:

```
j@llmbits:~$ curl -sS -o /dev/null -w 'http=%{http_code}\n' https://10.77.3.10:8443/
curl: (60) SSL certificate problem: self-signed certificate
More details here: https://curl.se/docs/sslcerts.html
http=000

j@llmbits:~$ curl -sS -k -o /dev/null -w 'http=%{http_code} tls=%{ssl_verify_result}\n' https://10.77.3.10:8443/
http=200 tls=18
```

`-k` did not fix anything, it just stopped caring. The `ssl_verify_result=18` is the OpenSSL code for a self-signed certificate, and it is still reported, which is why `%{ssl_verify_result}` belongs in your monitoring line even when you use `-k`.

To see what the server actually presented, filter the verbose output:

```
j@llmbits:~$ curl -k -v -o /dev/null https://10.77.3.10:8443/ 2>&1 | grep -E 'SSL|subject|issuer|start date|expire|ALPN' | head -20
* ALPN: curl offers h2,http/1.1
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519MLKEM768 / RSASSA-PSS
* ALPN: server did not agree on a protocol. Uses default.
*  subject: CN=srv1.pinglabz.lab
*  start date: Aug 19 15:00:16 2026 GMT
*  expire date: Aug 19 15:00:16 2027 GMT
*  issuer: CN=srv1.pinglabz.lab
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
```

Subject equal to issuer is the definition of self-signed. The expiry dates are there in plain text, which makes this a one-liner for "is that certificate about to lapse". The ALPN lines tell you whether HTTP/2 was negotiated: here the server declined and the connection fell back to HTTP/1.1.

## Downloading, and rate limiting

`-O` saves using the remote filename, `-o name` saves to a name you pick. `--limit-rate` caps throughput, which is genuinely useful when you are pulling an image across a link that is also carrying production traffic:

```
j@llmbits:~$ curl -sS --limit-rate 200k -o big.bin -w 'speed=%{speed_download} B/s total=%{time_total}s\n' http://10.77.3.10:8000/big.bin
speed=212168 B/s total=24.710979s
```

Five megabytes at a requested 200 KB/s took 24.7 seconds at a measured 212 KB/s. The cap holds, with the usual small overshoot from the first burst before the limiter settles.

For anything involving recursion, mirroring or unattended retries, use [wget](https://www.pinglabz.com/wget-linux/) instead. That is the split between the two tools and it has not changed in twenty years: `curl` is a request tool that happens to save files, `wget` is a download tool that happens to speak HTTP.

## Scripting curl without getting burned

A loop over ports, using the exit status and the status code together, is the closest thing `curl` has to a port scanner:

```
j@llmbits:~$ for p in 80 443 8000 8001; do printf '%s -> ' $p; curl -sS -o /dev/null -m 2 -w '%{http_code}\n' http://10.77.2.10:$p/ || true; done
80 -> 200
443 -> curl: (7) Failed to connect to 10.77.2.10 port 443 after 4 ms: Could not connect to server
000
8000 -> curl: (7) Failed to connect to 10.77.2.10 port 8000 after 4 ms: Could not connect to server
000
8001 -> curl: (7) Failed to connect to 10.77.2.10 port 8001 after 4 ms: Could not connect to server
000
```

Three flags stop most scripted `curl` from lying to you:

**`-f`**Fail on HTTP errors. Without it, `curl` exits 0 on a 500 and your script cheerfully saves the error page as if it were data. 

**`-m` and `--connect-timeout`**Total and connect ceilings. A monitoring check with no timeout will eventually hang forever against a black hole and take your scheduler with it. 

**`-sS`**Silence the progress bar, keep the errors. `-s` alone hides the reason it failed, which is how you end up with a cron job that fails invisibly for a month. 

One more, for credentials: never put a password in the command line, because it lands in your shell history and in `ps` output for every user on the box. Use `-u user` and let it prompt, or use a `~/.netrc` file with `--netrc`, or read the token from an environment variable with `-H "Authorization: Bearer $TOKEN"`.

## FAQ

### Should I use curl or wget?

`curl` for testing, probing and APIs, because it speaks more protocols, exposes the timing breakdown and writes to stdout by default. `wget` for fetching things, because it retries, resumes and mirrors recursively without you writing a loop. Most engineers end up with both and use each for one job.

### Why does curl report 200 but my script still breaks?

Because the status line and the body are different things. A load balancer sitting in front of a dead backend can return a perfectly valid 200 with an error page in it. Check `%{size_download}` and grep the body for a known-good string, rather than trusting the code alone.

### What is the difference between --connect-timeout and -m?

`--connect-timeout` caps only the time to establish the TCP connection. `-m` (or `--max-time`) caps the whole operation including the transfer. Use both: a small connect timeout finds dead hosts fast, and a larger max time keeps a slow but working download alive.

### How do I see which IP curl actually used?

`-w '%{remote_ip}:%{remote_port}\n'`, or read the `* Trying ...` line in `-v` output. On a dual-stack host this is how you catch `curl` preferring an AAAA record over a working A record, which is a very common cause of "it works for some people".

### Is -k ever acceptable?

In a lab, yes. Against production, treat it as a diagnostic step and not a fix: if `-k` makes it work, the answer is that the trust chain is broken and needs fixing, usually by installing the internal CA into `/etc/ssl/certs` or pointing `curl` at it with `--cacert`.

### Can curl test a port that is not HTTP?

It can connect with `telnet://host:port`, but you will get better error reporting from [netcat](https://www.pinglabz.com/netcat-nc-linux/) for a plain port check. Use `curl` when you want to know what the service said, not just that the socket opened.

## Key takeaways

- `curl -I` proves reachability plus a live service in one command, and does not flood your terminal with a page body.
- Fast failure means the network answered (reset or ICMP unreachable). Slow failure means something silently dropped your packet. Exit code 7 against exit code 28 is that distinction, in a script.
- The `-w` timing variables split DNS, TCP, TLS and server think time. That breakdown is what turns "the app is slow" into a ticket for the right team.
- Use `--resolve`, not a hand-written `Host:` header, when testing one node behind a name. It fixes SNI as well as the header.
- `-k` silences certificate validation but still reports `%{ssl_verify_result}`. Log that field even when you skip verification.
- In scripts, always use `-f`, always set a timeout, and use `-sS` rather than bare `-s` so failures stay visible.
- Keep passwords out of the command line. `~/.netrc` or an environment variable, never `-u user:pass` on a shared box.

Next in this cluster: [wget](https://www.pinglabz.com/wget-linux/) for retries, mirrors and unattended downloads, [netcat](https://www.pinglabz.com/netcat-nc-linux/) for port checks below the HTTP layer, and [SSH tunneling](https://www.pinglabz.com/ssh-tunneling-scp/) for reaching services `curl` cannot see from where you are standing. All of them, plus the rest of the toolset, are indexed on the [Linux networking commands](https://www.pinglabz.com/linux-networking-commands/) pillar.