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 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 msStart 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 zstdThat 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 URLThe 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:IPTest 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=200Now 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 millisecondsThe 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 intactThe 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: bytesTiming 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=896Read the gaps rather than the absolute numbers:
time_namelookupDNS. 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 DNSThe TCP handshake, which is one round trip. This is your real path latency, and it should track your ping RTT closely.
time_appconnectTLS handshake completion. The gap from time_connect is crypto and certificate work, typically one or two extra round trips.
time_starttransferFirst 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_totalEverything. 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, 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: bytesPeople 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 404For 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 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.710979sFive 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 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
000Three flags stop most scripted curl from lying to you:
-fFail 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-timeoutTotal and connect ceilings. A monitoring check with no timeout will eventually hang forever against a black hole and take your scheduler with it.
-sSSilence 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 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 -Iproves 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
-wtiming 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-writtenHost:header, when testing one node behind a name. It fixes SNI as well as the header. -ksilences 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-sSrather than bare-sso failures stay visible. - Keep passwords out of the command line.
~/.netrcor an environment variable, never-u user:passon a shared box.
Next in this cluster: wget for retries, mirrors and unattended downloads, netcat for port checks below the HTTP layer, and SSH tunneling 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 pillar.