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
FlagEffect
-L / --locationFollow Location headers
--max-redirs <n>Cap the hop count, default 50, -1 unlimited
--location-trustedFollow and keep sending -u credentials
--proto-redir <list>Which protocols a redirect may switch to
--post301 / --post302 / --post303Keep 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.

StatusDefault curl behaviorOverride
301POST → GET, body dropped--post301
302POST → GET, body dropped--post302
303POST → GET, body dropped--post303
307Method and body preserved—
308Method and body preserved—
curl -sL --post301 --post302 -d 'a=1' https://ex.com/legacy

-X POST -L is not the same fix. -X pins 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
FlagEffect
--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-connrefusedAlso retry a refused connection
--retry-all-errorsRetry 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 file is reset correctly, > file is not.

Timeouts

Retries without timeouts turn one hung request into a hung script. Always set both, they cover different phases.

FlagBounds
--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.