curl Cheatsheet

Output and Timing

Use this curl reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

Where the body goes

By default the response body goes to stdout and the progress meter to stderr. That split is what lets you pipe the body while still seeing progress.

FlagEffect
-o <file>Write the body to file, - means stdout
-O / --remote-nameUse the URL's last path segment as the filename
-J / --remote-header-nameUse the Content-Disposition filename
--output-dir <d>Directory for -o and -O (curl 7.73.0)
--create-dirsCreate missing directories for -o
-C -Resume a partial download
--remove-on-errorDelete the output file if the transfer fails
-s / --silentNo progress meter, no error text
--no-progress-meterHide progress, keep errors (curl 7.67.0)
-# / --progress-barSimple bar instead of the table
curl -O https://ex.com/dist/app-1.2.3.tgz
curl -o app.tgz --create-dirs --output-dir ./dl https://ex.com/app.tgz
curl -fL --remove-on-error -o app.tgz https://ex.com/app.tgz
curl -sS https://ex.com/data.json | jq '.items[0]'

-O uses the URL path, not what the server suggests, so a URL ending in / or ? gives you a useless name. -OJ uses Content-Disposition instead, which is why downloads from a redirecting CDN need it.

--write-out basics

-w prints a template after the transfer, to stdout, with %{var} for variables. Combine it with -s -o /dev/null to get numbers and nothing else.

curl -so /dev/null -w '%{http_code}\n' https://example.com
curl -so /dev/null -w 'code=%{http_code} bytes=%{size_download}\n' \
     https://example.com
curl -so /dev/null -w '%{json}' https://example.com | jq .
EscapePrints
\n \t \rNewline, tab, carriage return
%%A literal percent
%{var}A variable
%header{Name}One response header value (curl 7.84.0)
%output{file}Redirect the rest of the template to a file
@filenameRead the whole format from a file, @- for stdin
curl -so /dev/null -w '%header{server}\n' https://example.com
curl -so /dev/null -w '%{stderr}done\n' https://example.com

Useful -w variables

VariableValue
http_code / response_codeFinal HTTP status
http_version1.1, 2, 3
methodMethod actually used
url_effectiveURL after redirects
redirect_urlNext Location when not following
num_redirectsHops followed
num_headersResponse header count
num_connectsNew connections opened
remote_ip / remote_portAddress actually connected to
local_ip / local_portLocal side of the socket
schemeScheme used
content_typeResponse Content-Type
size_download / size_uploadBytes of body
size_header / size_requestBytes of headers, of the request
speed_download / speed_uploadBytes per second
ssl_verify_result0 on a verified certificate
http_connectProxy CONNECT status
filename_effectiveFile actually written
exitcode / errormsgcurl's exit code and message
json / header_jsonThe whole variable set, or all headers, as JSON
onerrorSkip the rest of the format unless the transfer failed
stderr / stdoutSend the rest of the template to that stream
curl -so /dev/null -w '%{onerror}FAILED %{exitcode} %{errormsg}\n' \
     https://does-not-exist.example/

The timing template

Every time_* variable is cumulative seconds since the request started, not a duration. Subtract neighbors to get per-phase cost.

VariableMarks the end of
time_namelookupDNS resolution
time_connectTCP handshake
time_appconnectTLS handshake, 0.000000 on plain HTTP
time_pretransferRequest fully sent, waiting begins
time_redirectAll redirect hops, 0 with no redirect
time_starttransferFirst response byte, this is TTFB
time_totalEverything

Save this as ~/.curl-format.txt and reuse it.

    time_namelookup:  %{time_namelookup}s\n
       time_connect:  %{time_connect}s\n
    time_appconnect:  %{time_appconnect}s\n
   time_pretransfer:  %{time_pretransfer}s\n
      time_redirect:  %{time_redirect}s\n
 time_starttransfer:  %{time_starttransfer}s\n
                    ----------\n
         time_total:  %{time_total}s\n
curl -w '@/Users/you/.curl-format.txt' -o /dev/null -s https://example.com

Reading it: a large time_namelookup is DNS, a large time_connect - time_namelookup is network latency or a SYN backlog, a large time_appconnect - time_connect is TLS handshake cost, and a large time_starttransfer - time_pretransfer is the server thinking. If time_total is much larger than time_starttransfer, the body itself is slow.

Scripting patterns

# status only, exit non-zero on 4xx/5xx
curl -fsS -o /dev/null -w '%{http_code}\n' https://ex.com/health

# every hop's timing, one line each
curl -sL -o /dev/null \
  -w '%{url_effective} %{http_code} %{time_total}\n' https://ex.com/old

# machine readable, straight into jq
curl -so /dev/null -w '%{json}' https://example.com \
  | jq '{code:.http_code, ttfb:.time_starttransfer, ip:.remote_ip}'

# body to a file, metrics to stdout
curl -s -o body.json -w '%{http_code} %{size_download}\n' https://ex.com/v1

# split streams: body to stdout, metrics to stderr
curl -s -w '%{stderr}%{http_code} in %{time_total}s\n' https://ex.com/v1

Remember exit codes: curl exits 0 for a 404 unless you pass -f, and --fail-with-body is the variant that keeps the error body so you can read the API's own explanation.