curl Cheatsheet
Redirects and Retries
Use this curl reference while you build software engineering projects, review code, or refresh the syntax you reach for most.
Following redirects
curl does not follow redirects by default. Without -L you get the 301 and an
empty or boilerplate body, which is the number one "curl returns nothing" report.
curl -sI https://example.com/old | head -1 # 301, no follow curl -sIL https://example.com/old | grep '^HTTP' # every hop's status curl -sL https://example.com/old -o page.html # follow and save
| Flag | Effect |
|---|---|
-L / --location | Follow Location headers |
--max-redirs <n> | Cap the hop count, default 50, -1 unlimited |
--location-trusted | Follow and keep sending -u credentials |
--proto-redir <list> | Which protocols a redirect may switch to |
--post301 / --post302 / --post303 | Keep POST across that status |
-w '%{url_effective}' | The final URL after all hops |
-w '%{num_redirects}' | How many hops were followed |
curl -sL --max-redirs 5 -o /dev/null \ -w '%{num_redirects} hops -> %{url_effective}\n' https://example.com/old
Method changes across a redirect
RFC 9110 says a client turns a POST into a GET on 303, and browsers do the same on 301 and 302. curl matches that, which means a redirected POST silently loses its body. 307 and 308 preserve the method and body by design.
| Status | Default curl behavior | Override |
|---|---|---|
| 301 | POST → GET, body dropped | --post301 |
| 302 | POST → GET, body dropped | --post302 |
| 303 | POST → GET, body dropped | --post303 |
| 307 | Method and body preserved | — |
| 308 | Method and body preserved | — |
curl -sL --post301 --post302 -d 'a=1' https://ex.com/legacy
-X POST -Lis not the same fix.-Xpins the verb on every hop but does not resend the body, so you get a POST with no payload. Use--post30x.
Protocol downgrades
A redirect from https:// to http:// would send the rest of the exchange in the
clear. curl allows it by default, so lock it down when the URL is not yours.
curl -sL --proto-redir =https https://ex.com/r # HTTPS only after a redirect curl -sL --proto '=https' https://ex.com/ # HTTPS for the first hop too curl -sL --proto-redir -all,https https://ex.com/r
The --proto syntax is a comma list with a prefix per entry: + adds, -
removes, and = sets the list to exactly that. -all,https is the readable way
to say "nothing but HTTPS".
Retries
--retry <n> retries only what curl considers transient. Anything else, including
a 404 or a 403, is not retried.
Retried by --retry: a timeout, an FTP 4xx, and HTTP 408, 429, 500,
502, 503, 504.
curl --retry 5 https://api.example.com/v1/jobs curl --retry 5 --retry-delay 2 https://api.example.com/v1/jobs curl --retry 5 --retry-max-time 60 https://api.example.com/v1/jobs
| Flag | Effect |
|---|---|
--retry <n> | Attempt up to n extra times, default 0 |
--retry-delay <s> | Fixed delay, disables exponential backoff |
--retry-max-time <s> | Wall clock cap on the whole retry loop |
--retry-connrefused | Also retry a refused connection |
--retry-all-errors | Retry on any error at all |
Default backoff waits 1 second, then doubles each time up to 10 minutes. curl
honors a Retry-After response header when the server sends one (7.66.0), which
is what makes --retry correct against a rate-limited API.
--retry-all-errors and its cost
--retry-all-errors (curl 7.71.0) retries every failure, including a TLS error or
a DNS failure that will never succeed. Reach for it only when you know the failure
mode is flaky.
curl --retry 5 --retry-all-errors --retry-max-time 120 \ -o release.tgz https://ex.com/release.tgz
The curl documentation calls it the sledgehammer. Do not put it in
.curlrc. With output redirected to a pipe or>file, a retried partial transfer is not reset, so you can end up with duplicated bytes.-o fileis reset correctly,> fileis not.
Timeouts
Retries without timeouts turn one hung request into a hung script. Always set both, they cover different phases.
| Flag | Bounds |
|---|---|
--connect-timeout <s> | DNS plus TCP plus TLS handshake |
-m / --max-time <s> | The entire operation, per URL |
--speed-limit <b> + --speed-time <s> | Abort if slower than b B/s for s seconds |
--happy-eyeballs-timeout-ms <ms> | How long to wait for IPv6 before trying IPv4 |
--keepalive-time <s> | TCP keepalive probe interval |
curl --connect-timeout 5 --max-time 30 --retry 3 -fsS https://ex.com/health curl --speed-limit 1024 --speed-time 30 -O https://ex.com/big.iso
A sane default for a scripted health check is
-fsS --connect-timeout 5 --max-time 30 --retry 3. Note that --max-time applies
per URL, so it does not cap the total when you retry, --retry-max-time does.