curl Cheat Sheet for Website Debugging
curl commands for debugging websites: headers, redirect chains, timing breakdowns, testing a new server before DNS changes, compression, HTTP/2 and HTTP/3, POST and JSON, plus PowerShell equivalents.
curl sends one HTTP request and shows you exactly what came back, with no browser cache, no extensions and no service worker in the way. That makes it the fastest way to answer questions like “is it the server or my browser?”, “how many redirects is this?”, “is the CDN caching this?” and “will the new server work before I change DNS?”. It ships with macOS, most Linux distributions and Windows 10 and 11 (as curl.exe).
Placeholders: https://EXAMPLE.COM/ is the URL you are testing, 203.0.113.10 stands for the new server’s IP address (a documentation address, replace it with yours), USERNAME and PASSWORD are credentials, and FILE.TXT is a file name. Quote URLs that contain & or ?, or the shell will split them. Some options below need a recent curl: check yours with curl --version, which also lists the protocols and features (HTTP2, HTTP3, brotli) your build supports.
Seeing the response: -I vs -i vs -v
| Option | Request sent | Output | Use for |
|---|---|---|---|
-I / --head |
HEAD | Response headers only | Quick status, Location and cache header checks. Some servers and plugins answer HEAD differently from GET. |
-i / --include |
GET (or whatever method you set) | Response headers, then the body | Seeing headers and content of the real request together. |
-v / --verbose |
GET | Connection details, TLS handshake, request headers (lines starting >), response headers (<), info (*), then the body |
Certificate problems, which IP was used, what curl actually sent. |
-s -D - -o /dev/null |
GET | Response headers only, body discarded | Headers of a real GET when HEAD gives misleading results. |
--trace-ascii FILE.TXT |
GET | Every byte sent and received, written to a file | Deep protocol debugging. |
-s / --silent |
n/a | Hides the progress meter and error messages | Scripts. Add -S (--show-error) to keep error messages. |
On Windows, use NUL instead of /dev/null.
Redirects
| Command | What it does |
|---|---|
curl -sI https://EXAMPLE.COM/ |
Shows only the first response. Read the status and Location header. |
curl -sIL https://EXAMPLE.COM/ |
-L (--location) follows every redirect and prints each response’s headers in turn. |
curl -sIL https://EXAMPLE.COM/ | grep -iE '^(HTTP|location)' |
The chain as a compact list of status lines and targets. |
curl -sL --max-redirs 5 -o /dev/null https://EXAMPLE.COM/ |
Gives up after 5 redirects (curl’s default limit is 50). Useful for proving a loop: curl exits with error 47. |
curl -s -o /dev/null -w '%{redirect_url}\n' https://EXAMPLE.COM/ |
Prints where the first redirect points, without following it. |
curl -sL -o /dev/null -w '%{num_redirects} %{url_effective}\n' https://EXAMPLE.COM/ |
Number of hops and the final URL. |
--location-trusted |
Like -L, but also sends credentials to other hosts in the chain. By default curl does not pass -u credentials to a different host. |
With -L, curl changes POST to GET after a 301, 302 or 303, as browsers do. Use --post301, --post302 or --post303 to keep POST.
Timing with -w (write-out)
-w (--write-out) prints variables after the transfer. Timing values are in seconds and each one is cumulative from the start of the request, so subtract to get the length of each phase.
| Variable | Meaning | A high value points to |
|---|---|---|
%{time_namelookup} |
Start until DNS resolution finished | Slow DNS provider or resolver |
%{time_connect} |
Start until the TCP connection was made | Network distance or a congested server |
%{time_appconnect} |
Start until the TLS handshake finished (0 for plain HTTP) | TLS setup, OCSP, long certificate chains |
%{time_pretransfer} |
Start until the request was about to be sent | Same as above |
%{time_starttransfer} |
Start until the first byte of the response arrived (time to first byte) | Slow PHP, database queries, no page cache |
%{time_redirect} |
Total time spent on redirects before the final request (with -L) |
Redirect chains |
%{time_total} |
Whole transfer | Large responses or slow bandwidth, if starttransfer was fast |
%{http_code} |
Status code of the last response (alias %{response_code}) |
n/a |
%{num_redirects} |
Redirects followed (needs -L) |
n/a |
%{redirect_url} |
The Location target when -L was not used |
n/a |
%{url_effective} |
The last URL fetched | n/a |
%{size_download} |
Body bytes downloaded (compressed size if compressed) | Heavy pages |
%{http_version} |
HTTP version used: 1.1, 2 or 3 | n/a |
%{remote_ip} |
IP address actually connected to | Confirms which server or CDN edge answered |
%{ssl_verify_result} |
0 means the certificate verified | n/a |
%{header_json} |
All response headers as JSON (curl 7.83.0 and later) | n/a |
%{json} |
All write-out variables except header_json as one JSON object (curl 7.70.0 and later) |
n/a |
A ready-made timing format
Save this as curl-format.txt, then run curl -s -o /dev/null -w @curl-format.txt https://EXAMPLE.COM/. The @ tells curl to read the format from a file.
http_code: %{http_code}\n
http_version: %{http_version}\n
remote_ip: %{remote_ip}\n
num_redirects: %{num_redirects}\n
redirect_url: %{redirect_url}\n
size_download: %{size_download} bytes\n
----- cumulative seconds -----\n
dns lookup: %{time_namelookup}\n
tcp connect: %{time_connect}\n
tls handshake: %{time_appconnect}\n
first byte: %{time_starttransfer}\n
total: %{time_total}\n
One-line version for quick checks:
curl -s -o /dev/null -w 'code=%{http_code} dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total} size=%{size_download}\n' https://EXAMPLE.COM/
Run it several times. The first request may include a cold cache or a DNS lookup that later requests skip. To see whether a page cache is working, compare time_starttransfer for a normal request with one that bypasses it (for example a request with a logged-in cookie or a unique query string).
Test a server before changing DNS
These options make curl connect to an IP or host you choose while still sending the real host name, so TLS (SNI), the certificate check and the Host header all behave exactly as they will after the DNS change.
| Option | Syntax | Example | Notes |
|---|---|---|---|
--resolve |
HOST:PORT:ADDRESS |
curl -sI --resolve example.com:443:203.0.113.10 https://example.com/ |
Pins a host name to an IP for this command only, like a one-off hosts file entry. Add one per host and port (for example both 80 and 443, and both www and bare names) when following redirects. |
--resolve with IPv6 |
Brackets around the address | --resolve example.com:443:[2001:db8::10] |
2001:db8:: is the IPv6 documentation range. |
--connect-to |
HOST1:PORT1:HOST2:PORT2 |
curl -sI --connect-to example.com:443:new-origin.example.net:443 https://example.com/ |
Connects to a different host name and port. Good for testing a CDN or load balancer hostname. Empty fields act as wildcards: ::203.0.113.10: sends every host and port to that IP. |
-H 'Host: ...' |
Header override | curl -sI -H 'Host: example.com' http://203.0.113.10/ |
Works for plain HTTP only. Over HTTPS the SNI and certificate check still use the IP in the URL, so prefer --resolve. |
Migration check: curl -s -o /dev/null -w '%{http_code} %{remote_ip} %{time_starttransfer}\n' --resolve example.com:443:203.0.113.10 https://example.com/. A 200 with the new IP in remote_ip and a valid certificate means the new server is ready for the switch.
Request headers
| Option | Example | Purpose |
|---|---|---|
-H / --header |
-H 'Accept-Language: es' |
Add or replace any request header. Repeat for several. |
| Remove a default header | -H 'User-Agent:' |
A header name with nothing after the colon removes it. |
-A / --user-agent |
-A 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)' |
Test mobile redirects, bot blocking and user-agent based caching. curl’s default is curl/VERSION, which some firewalls block. |
-e / --referer |
-e 'https://www.google.com/' |
Test hotlink protection and referrer-based redirects. -e ';auto' sets the referer automatically when following redirects. |
-b / --cookie |
-b 'name=value; other=value2' or -b cookies.txt |
Send cookies. With a file name, curl reads cookies from it. |
-c / --cookie-jar |
-c cookies.txt |
Save cookies the server sets, to reuse with -b. |
-u / --user |
-u USERNAME:PASSWORD |
HTTP basic auth, for example a password-protected staging site. Leave out :PASSWORD and curl prompts for it, which keeps it out of shell history. |
| Accept-Encoding | -H 'Accept-Encoding: br' |
Ask for one specific compression and check the Content-Encoding response header. The body stays compressed, so combine with -o /dev/null or -I. |
--compressed |
curl -s --compressed https://EXAMPLE.COM/ |
Asks for every compression method the build supports and decompresses the body for display. |
| Cache bypass | -H 'Cache-Control: no-cache' |
Asks caches to revalidate. Many CDNs ignore this from clients, so a unique query string (?nocache=1) is more reliable. |
Protocols, TLS and proxies
| Option | What it does | Notes |
|---|---|---|
--http1.1 |
Force HTTP/1.1 | Rules out HTTP/2 problems. |
--http2 |
Ask for HTTP/2 | Over HTTPS, negotiated with ALPN. Falls back to 1.1 if the server refuses. |
--http3 |
Try HTTP/3 (QUIC), falling back to earlier versions | Needs a curl build with HTTP3 in its feature list. |
--http3-only |
HTTP/3 or fail (curl 7.88.0 and later) | Proves whether HTTP/3 really works. |
-k / --insecure |
Skip certificate verification | Dangerous. The connection is still encrypted, but curl no longer checks who it is talking to, so anyone in the middle can impersonate the server. Use it only to look at a known broken certificate, never in scripts that send passwords or data, and never as a “fix”. |
--cacert FILE.PEM |
Trust a specific CA certificate | The safe way to test servers using a private or staging CA. |
--tlsv1.2, --tlsv1.3 |
Minimum TLS version | Combine with --tls-max 1.2 to test one exact version. |
-x / --proxy |
-x http://PROXYHOST:PORT |
Send the request through a proxy. socks5h:// also resolves DNS through the proxy. --noproxy '*' ignores proxy environment variables. |
-4, -6 |
Force IPv4 or IPv6 | Finds a broken AAAA record: the site works with -4 but not -6. |
--connect-timeout 5, -m 20 |
Time limits in seconds | -m (--max-time) caps the whole transfer. Keeps scripts from hanging. |
Read the certificate details with curl -sv -o /dev/null https://EXAMPLE.COM/ 2>&1 | grep -iE 'subject|issuer|expire|SSL connection'. Verbose output goes to stderr, which is why it is redirected.
POST, forms and JSON
| Command | Sends |
|---|---|
curl -d 'name=Ann&email=ann@example.com' https://EXAMPLE.COM/form |
POST with Content-Type: application/x-www-form-urlencoded. -d implies POST. |
curl --data-urlencode 'message=Hello & welcome' https://EXAMPLE.COM/form |
URL-encodes the value for you. |
curl -d @payload.txt https://EXAMPLE.COM/form |
Body read from a file. -d strips newlines; use --data-binary @FILE to send the file exactly. |
curl -F 'file=@photo.jpg' https://EXAMPLE.COM/upload |
Multipart form upload, like a browser file field. |
curl --json '{"title":"Test"}' https://EXAMPLE.COM/wp-json/wp/v2/posts |
POST with the data plus Content-Type: application/json and Accept: application/json (curl 7.82.0 and later). curl does not validate the JSON. |
curl -H 'Content-Type: application/json' -d '{"title":"Test"}' URL |
The same for older curl versions. |
curl -X DELETE URL |
Any other method. Do not combine -X HEAD with a GET; use -I. |
curl -s https://EXAMPLE.COM/wp-json/ | head -c 500 |
Quick check that the WordPress REST API answers with JSON and not an HTML error page. |
Downloading
| Command | What it does |
|---|---|
curl -O https://EXAMPLE.COM/backup.zip |
Save using the remote file name. |
curl -o FILE.ZIP URL |
Save under a name you choose. |
curl -C - -O URL |
Resume an interrupted download. -C - tells curl to work out the offset from the partial file. The server must support range requests. |
curl --retry 5 --retry-delay 3 -C - -O URL |
Retry on transient errors, resuming each time. |
curl -fsS -o FILE URL |
-f (--fail) makes curl exit with an error on 4xx and 5xx instead of saving the error page as your file. |
curl --limit-rate 1M -O URL |
Cap bandwidth so a large download does not saturate a shared connection. |
Testing a list of redirects from a CSV
Create redirects.csv with one old_url,expected_url pair per line and no header row. The loop below checks each old URL returns a 301 pointing at the expected target.
#!/usr/bin/env bash
# usage: bash check-redirects.sh redirects.csv
while IFS=, read -r from expected; do
from=$(printf '%s' "$from" | tr -d '\r')
expected=$(printf '%s' "$expected" | tr -d '\r')
[ -z "$from" ] && continue
result=$(curl -s -o /dev/null --max-time 15 -w '%{http_code} %{redirect_url}' "$from")
code=${result%% *}
target=${result#* }
if [ "$code" = "301" ] && [ "$target" = "$expected" ]; then
echo "OK $from"
else
echo "FAIL $from got $code $target expected 301 $expected"
fi
done < "$1"
The tr -d '\r' lines strip Windows line endings from spreadsheets, which otherwise make every comparison fail. To check the final destination instead of the first hop, add -L and compare %{url_effective} with %{http_code} 200.
PowerShell equivalents
In Windows PowerShell 5.1, curl is an alias for Invoke-WebRequest, which has different options. Type curl.exe to run real curl, which Windows 10 and 11 include. PowerShell 7 has no curl alias, so curl runs the real program there. Several parameters below exist only in PowerShell 6 or 7 and later, as marked.
| Task | curl | PowerShell |
|---|---|---|
| Headers only | curl -sI URL |
(Invoke-WebRequest -Uri URL -Method Head).Headers |
| Status code | -w '%{http_code}' |
(Invoke-WebRequest -Uri URL).StatusCode |
| Do not follow redirects | default | Invoke-WebRequest -Uri URL -MaximumRedirection 0 -SkipHttpErrorCheck (7+). Read .Headers.Location. |
| Follow redirects | -L |
Default behavior. -MaximumRedirection 5 caps it. |
| 4xx and 5xx without an exception | default | -SkipHttpErrorCheck (7+) |
| Custom headers | -H 'Accept-Language: es' |
-Headers @{ 'Accept-Language' = 'es' } |
| User agent | -A 'UA' |
-UserAgent 'UA' |
| Referer | -e URL |
-Headers @{ Referer = 'URL' } |
| Basic auth | -u USERNAME |
-Credential (Get-Credential) -Authentication Basic (6+; add -AllowUnencryptedAuthentication only for plain HTTP) |
| POST form | -d 'a=1&b=2' |
-Method Post -Body @{ a = '1'; b = '2' } |
| POST JSON | --json '{...}' |
-Method Post -ContentType 'application/json' -Body '{"title":"Test"}', or Invoke-RestMethod to get parsed JSON back |
| Download | -o FILE |
-OutFile FILE |
| Resume download | -C - |
-OutFile FILE -Resume (6.1+) |
| Skip certificate check | -k |
-SkipCertificateCheck (6+). Same danger as -k. |
| Proxy | -x URL |
-Proxy URL |
| HTTP version | --http2 |
-HttpVersion 2.0 (7.3+) |
| Total time | -w '%{time_total}' |
(Measure-Command { Invoke-WebRequest -Uri URL }).TotalMilliseconds. There is no per-phase breakdown; use curl.exe for that. |
| Test against a new IP | --resolve |
No direct equivalent. Use curl.exe --resolve, or a temporary hosts file entry in C:\Windows\System32\drivers\etc\hosts. |
In Windows PowerShell 5.1, add -UseBasicParsing to avoid a dependency on the Internet Explorer engine. PowerShell 6 and later always use basic parsing.
Common recipes
| Question | Command |
|---|---|
| Is the CDN caching this page? | curl -sI https://EXAMPLE.COM/ | grep -iE 'cf-cache-status|x-cache|age|cache-control' (run twice: the second should show a HIT and a rising Age) |
| Is the page cache plugin working? | curl -sI https://EXAMPLE.COM/ | grep -iE 'x-litespeed-cache|x-cache|cache', then compare time_starttransfer for two runs |
| Which security headers are set? | curl -sI https://EXAMPLE.COM/ | grep -iE 'strict-transport|content-security|x-frame|x-content-type|referrer-policy|permissions-policy' |
| Does HTTP go to HTTPS in one hop? | curl -sIL http://EXAMPLE.COM/ | grep -iE '^(HTTP|location)' |
| Is compression on? | curl -sI -H 'Accept-Encoding: gzip, br' https://EXAMPLE.COM/ | grep -i content-encoding |
| Is the site down or just slow? | curl -s -o /dev/null --connect-timeout 5 -m 30 -w '%{http_code} %{time_total}\n' https://EXAMPLE.COM/ (code 000 means no HTTP response at all) |
| Does the site block curl? | Compare the default user agent with -A set to a browser string. A 403 only for the default points to a firewall rule. |
| Is wp-cron reachable? | curl -s -o /dev/null -w '%{http_code}\n' https://EXAMPLE.COM/wp-cron.php |
| Watch a page during an outage | while true; do curl -s -o /dev/null -w "$(date +%T) %{http_code} %{time_total}\n" https://EXAMPLE.COM/; sleep 10; done |
| Pretty-print a JSON response | curl -s https://EXAMPLE.COM/wp-json/ | python3 -m json.tool | head -40 |
Gotchas
| Problem | Explanation |
|---|---|
| HEAD and GET disagree | Some applications and firewalls treat HEAD specially or reject it. Confirm with -s -D - -o /dev/null. |
| Binary garbage in the terminal | You sent Accept-Encoding by hand without --compressed, so curl printed the compressed bytes. |
URL cut off at & |
The shell treated & as “run in background”. Quote the URL. |
| Status 000 | No HTTP response: DNS failure, connection refused, timeout or a TLS error. Rerun with -v and read the curl exit code (echo $?). |
| Common exit codes | 6 could not resolve host, 7 could not connect, 22 HTTP error with -f, 28 timeout, 35 TLS handshake failed, 47 too many redirects, 60 certificate could not be verified. |
| Different result from the browser | The browser may have cached a 301, be logged in (cookies), use a service worker or reach a different IP. curl starts clean every time. |
--resolve ignored after a redirect |
The redirect went to a different host or port. Add a --resolve entry for each host and port in the chain. |
| Timing values look backwards | They are cumulative. DNS time is time_namelookup, but TLS time alone is time_appconnect minus time_connect. |
| Passwords in shell history | Avoid -u USERNAME:PASSWORD on shared machines. Let curl prompt, or use a .netrc file with --netrc and permissions 600. |
Did this fix it?
Still stuck? Start again from what you can see in the Emergency Room.
Need the full fix?
Cheat sheets are quick reference. The field guides walk you through each emergency step by step.
Open the Emergency Room