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.
| Flag | Effect |
|---|---|
-o <file> | Write the body to file, - means stdout |
-O / --remote-name | Use the URL's last path segment as the filename |
-J / --remote-header-name | Use the Content-Disposition filename |
--output-dir <d> | Directory for -o and -O (curl 7.73.0) |
--create-dirs | Create missing directories for -o |
-C - | Resume a partial download |
--remove-on-error | Delete the output file if the transfer fails |
-s / --silent | No progress meter, no error text |
--no-progress-meter | Hide progress, keep errors (curl 7.67.0) |
-# / --progress-bar | Simple 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 .
| Escape | Prints |
|---|---|
\n \t \r | Newline, 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 |
@filename | Read 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
| Variable | Value |
|---|---|
http_code / response_code | Final HTTP status |
http_version | 1.1, 2, 3 |
method | Method actually used |
url_effective | URL after redirects |
redirect_url | Next Location when not following |
num_redirects | Hops followed |
num_headers | Response header count |
num_connects | New connections opened |
remote_ip / remote_port | Address actually connected to |
local_ip / local_port | Local side of the socket |
scheme | Scheme used |
content_type | Response Content-Type |
size_download / size_upload | Bytes of body |
size_header / size_request | Bytes of headers, of the request |
speed_download / speed_upload | Bytes per second |
ssl_verify_result | 0 on a verified certificate |
http_connect | Proxy CONNECT status |
filename_effective | File actually written |
exitcode / errormsg | curl's exit code and message |
json / header_json | The whole variable set, or all headers, as JSON |
onerror | Skip the rest of the format unless the transfer failed |
stderr / stdout | Send 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.
| Variable | Marks the end of |
|---|---|
time_namelookup | DNS resolution |
time_connect | TCP handshake |
time_appconnect | TLS handshake, 0.000000 on plain HTTP |
time_pretransfer | Request fully sent, waiting begins |
time_redirect | All redirect hops, 0 with no redirect |
time_starttransfer | First response byte, this is TTFB |
time_total | Everything |
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\ncurl -w '@/Users/you/.curl-format.txt' -o /dev/null -s https://example.comReading 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.