Skip to content

curl Folha de referência

Command-line tool for transferring data with URLs.

01

Getting Started

Basic Requests

curl makes HTTP requests from the command line. By default it does GET. -v shows verbose output, -o saves to a named file, -O keeps the remote filename, -L follows redirects, -I fetches only headers (HEAD request).

curl
# GET request (default)
curl https://example.com

# with verbose output
curl -v https://example.com

# save output to file
curl -o output.html https://example.com
curl -O https://example.com/file.zip   # keep remote filename

# follow redirects
curl -L https://example.com/redirect

# show only headers
curl -I https://example.com
curl --head https://example.com

Output & Save

-o writes the body to a named file; -O uses the remote filename. --output-dir sets the destination directory. -s (silent) suppresses the progress meter and errors — pair with -S to still show errors. Use -D to dump headers to a file while discarding the body with -o /dev/null.

curl
# print body to stdout (default)
curl https://example.com

# save body to file
curl -o page.html https://example.com

# save with remote filename
curl -O https://example.com/file.zip

# save to specific dir with remote name
curl -O --output-dir /tmp https://example.com/file.zip

# append to file
curl https://example.com >> log.txt

# discard body, keep headers
curl -o /dev/null -D headers.txt https://example.com

# silent (no progress, no errors)
curl -s https://example.com

Verbose & Headers

-v (verbose) prints the full conversation: '>' is the request, '<' is the response, '*' is curl info — all sent to stderr so stdout stays clean for the body. -I sends a HEAD request and prints headers. -D dumps headers to a file or '-' for stdout; combine with -o /dev/null to inspect headers without saving the body.

curl
# verbose: shows request + response, sent to stderr
curl -v https://example.com

# > lines are the request, < lines are the response
# * lines are curl's internal info

# headers only (HEAD request)
curl -I https://example.com

# dump response headers to file
curl -D headers.txt -o body.html https://example.com

# dump headers to stdout, body to file
curl -D - -o body.html https://example.com

# show only response headers (no HEAD)
curl -s -o /dev/null -D - https://example.com

Following Redirects

-L follows HTTP 3xx redirects; by default curl follows up to 50 hops (use --max-redirs to cap). RFC 301/302 typically converts POST to GET — use --post301/--post302 to preserve the method. By default credentials are not sent to redirected hosts; --location-trusted sends them everywhere (security risk).

curl
# follow 3xx redirects
curl -L https://example.com/old

# limit number of redirects
curl -L --max-redirs 5 https://example.com

# treat redirects as POST (default resends as GET)
curl -L --post301 --post302 https://example.com

# show each hop in a redirect chain
curl -L -v https://short.url/abc 2>&1 | grep -i location

# follow only http/https redirects
curl -L --proto-redir http,https https://example.com

# send credentials only on first request
curl -L --location-trusted -u user:pass https://example.com

URL Syntax & Globbing

curl expands URL 'globbing' patterns: [1-10] ranges, {a,b,c} lists, with an optional :step. Each expansion is a separate request. Use -g (--globoff) to treat brackets and braces literally — important for URLs containing JSON arrays or IPv6 addresses like http://[::1]:8080/.

curl
# range glob: requests file1, file2, file3
curl -O https://example.com/file[1-3].zip

# range with step
curl -O https://example.com/file[1-10:2].zip   # 1,3,5,7,9

# list glob: a, b, c
curl -O https://example.com/{a,b,c}.zip

# combined globs
curl -O https://example.com/{v1,v2}/file[1-2].txt

# output each URL as it is fetched
curl -O https://example.com/file[1-3].zip --write-out "%{url} -> %{filename}\n"

# disable globbing (literal brackets/braces)
curl -g "https://example.com/file[1].zip"

Help & Version

--version shows the curl version, supported protocols (HTTP, HTTPS, FTP, etc.), and compiled-in features (HTTP2, SSL, brotli). --help all lists every option — useful when you remember part of a flag name. Features depend on how curl was built (e.g., HTTP/3 needs a special build).

curl
# version and supported features
curl --version

# short help (one-line per option)
curl --help

# long help (all options with details)
curl --help all

# search help for a keyword
curl --help all | grep -i cookie

# list supported protocols
curl --version | head -1

# check if a feature is built in
curl --version | grep -i http2
02

HTTP Methods

GET Requests

GET is the default method. Always quote URLs containing & or ? so the shell doesn't background or expand them. -X explicitly sets the method but is usually unnecessary for GET. For APIs, set Accept to negotiate the response format.

curl
# simple GET (default method)
curl https://api.example.com/data

# GET with query string
curl "https://api.example.com/search?q=curl&page=2"

# explicit method
curl -X GET https://example.com

# GET with headers
curl -H "Accept: application/json" https://api.example.com/data

# GET with auth
curl -u user:pass https://api.example.com/secret

POST Requests

Sending -d automatically switches the method to POST, so -X POST is redundant (but harmless and clarifying). To POST a file, use -d @filename (the @ reads the file). The default Content-Type for -d is application/x-www-form-urlencoded; override with -H for JSON.

curl
# POST with form data (application/x-www-form-urlencoded)
curl -X POST -d "name=Alice&age=30" https://api.example.com/users

# -d implies POST, so -X is optional
curl -d "name=Alice" https://api.example.com/users

# POST with no body
curl -X POST https://api.example.com/trigger

# POST JSON
curl -X POST -H "Content-Type: application/json" \
     -d '{"name":"Alice"}' https://api.example.com/users

# POST with data from a file
curl -X POST -d @form.txt https://api.example.com/users

PUT & PATCH

PUT replaces a whole resource; PATCH applies a partial update. Both require -X because curl has no -d shortcut for them. -T (upload-file) streams a file as the request body and is ideal for PUT uploads. Use --data-binary instead of -d to avoid stripping newlines when sending raw files.

curl
# PUT (replace entire resource)
curl -X PUT -H "Content-Type: application/json" \
     -d '{"name":"Alice","age":31}' \
     https://api.example.com/users/1

# PATCH (partial update)
curl -X PATCH -H "Content-Type: application/json" \
     -d '{"age":32}' \
     https://api.example.com/users/1

# PUT a file as body
curl -X PUT --data-binary @config.json \
     -H "Content-Type: application/json" \
     https://api.example.com/config

# PUT with upload file
curl -X PUT -T localfile.txt https://example.com/remote.txt

DELETE

DELETE removes a resource. Most APIs require authentication, so include -u or an Authorization header. Some APIs accept a body (reason, cascade flags) — add -d and a Content-Type header. Use -w '%{http_code}' to check the result: 204 (No Content) and 200 are typical successes.

curl
# delete a resource
curl -X DELETE https://api.example.com/users/1

# delete with auth
curl -X DELETE -H "Authorization: Bearer TOKEN" \
     https://api.example.com/users/1

# delete with confirmation check
curl -X DELETE -w "%{http_code}" -o /dev/null -s \
     https://api.example.com/users/1

# delete with request body (some APIs require it)
curl -X DELETE -H "Content-Type: application/json" \
     -d '{"reason":"spam"}' \
     https://api.example.com/users/1

HEAD & OPTIONS

-I sends a HEAD request (headers only, no body) — useful for checking size (Content-Length), type (Content-Type), and last-modified without downloading. OPTIONS reveals allowed methods (the Allow header) and is used for CORS preflight by browsers. Add Origin and Access-Control-Request-Method headers to simulate a real preflight.

curl
# HEAD: headers only, no body
curl -I https://example.com
curl --head https://example.com

# OPTIONS: discover allowed methods
curl -X OPTIONS https://api.example.com/users

# OPTIONS with CORS preflight headers
curl -X OPTIONS \
     -H "Origin: https://mysite.com" \
     -H "Access-Control-Request-Method: POST" \
     https://api.example.com/users

# check allowed methods from Allow header
curl -X OPTIONS -s -D - -o /dev/null https://api.example.com | grep -i allow
03

Headers

Setting Headers (-H)

-H (--header) adds a request header. Repeat -H for multiple headers. A trailing semicolon (X-Empty;) sends an empty value. @headers.txt reads headers from a file (one 'Name: value' per line) — handy for keeping Authorization tokens out of shell history.

curl
# single header
curl -H "Accept: application/json" https://api.example.com/data

# multiple headers
curl -H "Accept: application/json" \
     -H "Authorization: Bearer TOKEN" \
     -H "X-Custom: value" \
     https://api.example.com/data

# header with empty value
curl -H "X-Empty;" https://example.com

# headers from a file (one per line)
curl -H @headers.txt https://example.com

# Content-Type for POST
curl -X POST -H "Content-Type: application/json" \
     -d '{"a":1}' https://api.example.com

Common Headers

The most common headers: Accept tells the server what format you want; Content-Type describes the request body; Authorization carries credentials. --compressed asks curl to request compressed responses (gzip, br) and auto-decompress them, so you don't need to handle encoding manually.

curl
# Accept: negotiate response format
curl -H "Accept: application/json" https://api.example.com
curl -H "Accept: text/html" https://example.com

# Content-Type: describe request body
curl -H "Content-Type: application/json" -d '{"a":1}' https://api.example.com
curl -H "Content-Type: application/x-www-form-urlencoded" -d "a=1" https://api.example.com

# Authorization: credentials
curl -H "Authorization: Bearer TOKEN" https://api.example.com

# Accept-Encoding: ask for compression
curl -H "Accept-Encoding: gzip, br" --compressed https://example.com

# Cache-Control
curl -H "Cache-Control: no-cache" https://api.example.com

User-Agent

Some servers block the default curl User-Agent. -A (--user-agent) sets it; an empty -A removes it entirely. The long form is -H 'User-Agent: ...'. Many scraping-blocking sites check for real browser signatures, so a realistic browser UA is often required.

curl
# default User-Agent looks like: curl/8.7.1
curl -v https://example.com 2>&1 | grep -i user-agent

# set a custom User-Agent
curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" https://example.com
curl -H "User-Agent: MyBot/1.0" https://example.com

# empty User-Agent
curl -A "" https://example.com

# fake a real browser (some sites require it)
curl -A "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) \
AppleWebKit/537.36 (KHTML, like Gecko) \
Chrome/120.0 Safari/537.36" https://example.com

# per-site default via config
# user-agent = "MyBot/1.0"  in ~/.curlrc

Referer & Host

-e (--referer) sets the Referer header. Overriding Host is essential when testing virtual hosts by IP: the server picks the right site based on Host. CORS-aware endpoints often require Origin; X-Forwarded-For/Proto simulate requests behind a proxy or load balancer.

curl
# set Referer header
curl -H "Referer: https://google.com" https://example.com/page
curl -e https://google.com https://example.com/page

# set Host header (override DNS)
curl -H "Host: api.example.com" https://192.168.1.10/users

# combined: test a vhost by IP with custom Host
curl -H "Host: api.example.com" \
     -H "X-Forwarded-Proto: https" \
     http://10.0.0.5/health

# Origin header (for CORS/POST forms)
curl -H "Origin: https://mysite.com" \
     -X POST -d "x=1" https://api.example.com/form

# X-Forwarded-For (debugging proxies)
curl -H "X-Forwarded-For: 1.2.3.4" https://api.example.com

Removing Headers

Providing -H 'Header-Name:' (with a colon but no value) tells curl to send no value for that header, effectively removing a default header like User-Agent or Accept-Encoding. curl replaces a header if you specify the same name again rather than sending duplicates. Useful for sending the bare minimum request.

curl
# remove a header by giving it no value
curl -H "User-Agent:" https://example.com

# remove Accept-Encoding (disable compression)
curl -H "Accept-Encoding:" https://example.com

# remove all auto-added headers and send minimal request
curl -H "Host:" -H "User-Agent:" -H "Accept:" https://example.com

# prevent curl from adding Accept-Encoding
curl --no-keepalive https://example.com

# replace a default header instead of adding a duplicate
# (curl replaces, doesn't duplicate, when -H name matches)
curl -H "Accept: text/plain" https://example.com
04

POST Data & Forms

Form Data (-d)

-d (--data) sends URL-encoded form data and switches the method to POST. Multiple -d flags are joined with &. -d strips leading/trailing whitespace and newlines; --data-binary preserves them; --data-raw disables the @file feature entirely (so a literal value starting with @ is sent as-is).

curl
# URL-encoded form data
curl -d "name=Alice&age=30" https://api.example.com/form

# explicit POST (same result; -d implies POST)
curl -X POST -d "name=Alice" https://api.example.com/form

# multiple -d are joined with &
curl -d "name=Alice" -d "age=30" https://api.example.com/form

# -d strips newlines; use --data-binary to preserve them
curl --data-binary "line1\nline2" https://api.example.com

# raw data, no content-type added
curl --data-raw "name=Alice&age=30" https://api.example.com

# default Content-Type: application/x-www-form-urlencoded
curl -v -d "a=1" https://api.example.com 2>&1 | grep -i content-type

URL-encoding

--data-urlencode percent-encodes the value (or the whole name=value), so you don't have to manually encode spaces, &, =, or non-ASCII. With name@file, the file's contents are URL-encoded as the value. Always use this for user-supplied data to avoid breaking the form structure.

curl
# spaces and special chars must be encoded
curl -d "name=Alice%20Smith" https://api.example.com
curl -d "name=Alice+Smith" https://api.example.com

# --data-urlencode encodes the value for you
curl --data-urlencode "name=Alice Smith" https://api.example.com
curl --data-urlencode "[email protected]" https://api.example.com

# encode name=value pair (both encoded)
curl --data-urlencode "name=Alice & Bob" https://api.example.com

# encode only the value after =, with explicit content-type
curl --data-urlencode "query=SELECT * FROM users" \
     -H "Content-Type: application/x-www-form-urlencoded" \
     https://api.example.com/search

# encode content of a file
curl --data-urlencode [email protected] https://api.example.com

Binary Data

--data-binary sends a file byte-for-byte (preserving newlines and null bytes), unlike -d which strips them. -T streams the file, so memory use stays low for large uploads. To send pre-compressed data, pipe it in with -H 'Content-Encoding: gzip' — the server must support this; otherwise use Transfer-Encoding: chunked.

curl
# send a binary file as the body
curl --data-binary @image.png https://api.example.com/upload

# upload with explicit content type
curl --data-binary @image.png \
     -H "Content-Type: image/png" \
     https://api.example.com/upload

# raw bytes (no @ expansion, no stripping)
curl --data-raw "$(printf '\x00\x01\x02')" https://api.example.com

# stream a large file without buffering in memory
curl -T bigfile.iso https://api.example.com/upload

# send gzipped body
gzip -c data.json | curl --data-binary @- \
     -H "Content-Encoding: gzip" \
     -H "Content-Type: application/json" \
     https://api.example.com/data

Reading from File (@file)

-d @file reads the request body from a file (the @ triggers file reading). --data-binary @file preserves the bytes. -d @- reads from stdin — perfect for piping jq, curl, or other transformations. If a literal value starts with @ and you don't want file expansion, use --data-raw.

curl
# read body from a file
curl -d @data.txt https://api.example.com/form

# read JSON body from a file
curl -H "Content-Type: application/json" \
     -d @payload.json https://api.example.com

# binary-safe file read
curl --data-binary @image.png https://api.example.com/upload

# read from stdin
echo '{"a":1}' | curl -d @- https://api.example.com

# pipe through curl (transform then send)
jq '{name, age}' input.json | \
  curl -H "Content-Type: application/json" -d @- https://api.example.com

# file name starting with @ (use --data-raw)
curl --data-raw "@mention" https://api.example.com

Content-Type & Forms

Always match Content-Type to the body format: application/x-www-form-urlencoded (default for -d), application/json (with -H), multipart/form-data (auto-set by -F), text/xml for SOAP. Adding charset=utf-8 is good practice for non-ASCII data. Mismatches cause servers to reject or misparse the body.

curl
# default for -d: application/x-www-form-urlencoded
curl -d "name=Alice" https://api.example.com

# JSON body
curl -H "Content-Type: application/json" \
     -d '{"name":"Alice"}' https://api.example.com

# multipart form (file upload, see File Upload section)
curl -F "[email protected]" https://api.example.com/upload

# text/plain body
curl -H "Content-Type: text/plain" -d "hello" https://api.example.com

# XML body (SOAP)
curl -H "Content-Type: text/xml" \
     -d '<?xml version="1.0"?><env:Envelope/>' \
     https://api.example.com/soap

# set charset explicitly
curl -H "Content-Type: application/json; charset=utf-8" \
     -d '{"name":"éàç"}' https://api.example.com
05

JSON Requests

Basic JSON POST

JSON requests need -H 'Content-Type: application/json' plus -d with a JSON string. Use single quotes around the JSON so the shell doesn't touch double quotes inside. Pipe the response through jq '.' for readable output, or jq '.field' to extract a specific value.

curl
# JSON POST request
curl -X POST \
     -H "Content-Type: application/json" \
     -d '{"name":"Alice","age":30}' \
     https://api.example.com/users

# -d already implies POST, so -X is optional
curl -H "Content-Type: application/json" \
     -d '{"name":"Alice"}' \
     https://api.example.com/users

# ask for JSON back
curl -H "Accept: application/json" \
     -H "Content-Type: application/json" \
     -d '{"name":"Alice"}' \
     https://api.example.com/users

# pretty-print the JSON response (pipe to jq)
curl -s -H "Accept: application/json" \
     https://api.example.com/users/1 | jq .

JSON from File

For complex JSON, store it in a .json file and use -d @file (or --data-binary @file to preserve exact bytes including trailing newlines). Piping jq -n lets you build JSON programmatically without fiddly escaping. Always validate with jq . first — a syntax error wastes a request.

curl
# POST JSON body from a file
curl -H "Content-Type: application/json" \
     -d @payload.json https://api.example.com/users

# binary-safe (preserves exact bytes)
curl -H "Content-Type: application/json" \
     --data-binary @payload.json https://api.example.com/users

# generate JSON with jq, then POST
jq -n '{name: "Alice", age: 30}' | \
  curl -H "Content-Type: application/json" \
       -d @- https://api.example.com/users

# validate JSON before sending
jq . payload.json && \
  curl -H "Content-Type: application/json" \
       -d @payload.json https://api.example.com/users

# send a JSON array
curl -H "Content-Type: application/json" \
     -d '[{"id":1},{"id":2}]' https://api.example.com/batch

JSON with Variables

Interpolating variables into JSON strings breaks on quotes and special chars. The safe pattern is jq --arg/--argjson: it builds valid JSON and escapes values correctly. --arg for strings, --argjson for numbers/booleans. For secrets, prefer a header file (-H @file) over command-line args to avoid leaking via ps.

curl
# interpolate a shell variable into JSON (risky for special chars)
NAME="Alice"
curl -H "Content-Type: application/json" \
     -d "{\"name\":\"$NAME\"}" https://api.example.com

# safer: build JSON with jq
NAME="Alice"
jq -n --arg name "$NAME" '{name: $name}' | \
  curl -H "Content-Type: application/json" -d @- https://api.example.com

# multiple variables
NAME="Alice"; AGE=30
jq -n --arg name "$NAME" --argjson age "$AGE" \
  '{name: $name, age: $age}' | \
  curl -H "Content-Type: application/json" -d @- https://api.example.com

# read secret from env, hide from ps
TOKEN="$API_TOKEN" curl -H "Authorization: Bearer $API_TOKEN" \
     -d '{"x":1}' https://api.example.com

GraphQL Requests

GraphQL uses a single POST endpoint with a JSON body containing a 'query' string and optional 'variables'. Writing the query inline requires escaping inner double quotes, so store complex queries in a .json file and use -d @file. Introspection (the __schema query) lists the API's types and is great for exploration.

curl
# GraphQL query as JSON body
curl -X POST \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer TOKEN" \
     -d '{"query":"{ user(id:1) { name email } }"}' \
     https://api.example.com/graphql

# query with variables
curl -X POST \
     -H "Content-Type: application/json" \
     -d '{"query":"query($id:Int!){ user(id:$id){name} }","variables":{"id":1}}' \
     https://api.example.com/graphql

# query from a file (multi-line, no escaping pain)
curl -X POST \
     -H "Content-Type: application/json" \
     -d @query.json https://api.example.com/graphql

# introspection: list all types
curl -s -X POST -H "Content-Type: application/json" \
     -d '{"query":"{ __schema { types { name } } }"}' \
     https://api.example.com/graphql | jq

Content Negotiation

Accept negotiates the response format; many REST APIs return JSON by default but support XML or CSV alternatives. --compressed requests gzip/brotli and decompresses automatically. APIs often version via Accept (e.g., GitHub: application/vnd.github+json). Check the actual Content-Type the server returns to confirm negotiation worked.

curl
# request JSON
curl -H "Accept: application/json" https://api.example.com/users

# request XML
curl -H "Accept: application/xml" https://api.example.com/users

# request a specific version
curl -H "Accept: application/vnd.api+json; version=2" \
     https://api.example.com/users

# request anything (default is */*)
curl -H "Accept: */*" https://example.com

# request compression and auto-decompress
curl --compressed -H "Accept-Encoding: gzip, br" https://example.com

# inspect what the server actually returns
curl -s -o /dev/null -D - https://api.example.com/users | grep -i content-type
06

File Upload

Multipart Upload (-F)

-F (--form) builds a multipart/form-data request — the standard for file uploads. -F file=@path reads the file. Add ;filename= to override the name the server sees, and ;type= to set the part's Content-Type. Multiple files in the same field name (with [] in many frameworks) need multiple -F flags.

curl
# upload a file as a multipart form field
curl -F "[email protected]" https://api.example.com/upload

# -F sets POST + multipart/form-data automatically
# the @ prefix reads the file

# add a text field alongside the file
curl -F "[email protected]" -F "description=My photo" \
     https://api.example.com/upload

# rename the uploaded file (server sees "avatar.jpg")
curl -F "[email protected];filename=avatar.jpg" \
     https://api.example.com/upload

# set explicit content-type for the file part
curl -F "[email protected];type=image/jpeg" \
     https://api.example.com/upload

# upload multiple files in one field
curl -F "[email protected]" -F "[email protected]" \
     https://api.example.com/upload

Multiple Files & Fields

Multipart forms can mix files and text fields. Use [] in field names for array-style uploads (frameworks like Rails/PHP parse these into arrays). A field can carry JSON by setting ;type=application/json — the server reads it as structured data rather than a file. Curl can't natively upload directories; tar them first.

curl
# upload two files and a caption
curl -F "[email protected]" \
     -F "[email protected]" \
     -F "caption=Summer trip" \
     https://api.example.com/gallery

# array-style field names (PHP/Rails style)
curl -F "files[][email protected]" -F "files[][email protected]" \
     https://api.example.com/upload

# send a file's contents as a regular text field
curl -F "[email protected];type=application/json" \
     https://api.example.com/upload

# mix file and JSON fields
curl -F "[email protected]" \
     -F "options={\"priority\":\"high\"};type=application/json" \
     https://api.example.com/upload

# upload a directory as a tar (workaround)
tar -cf - mydir/ | curl -F "tar=@-;filename=dir.tar" \
     https://api.example.com/upload

Streaming Upload (-T)

-T (--upload-file) uses PUT and streams the file, so memory stays flat even for huge uploads — far better than -d @file for big data. -T - reads from stdin. -C - resumes an interrupted transfer. With a trailing slash on the URL, curl appends the local filename (like HTTP PUT mirroring).

curl
# upload a file with PUT (streaming, low memory)
curl -T localfile.txt https://example.com/upload/remote.txt

# upload to a URL with the local filename
curl -T localfile.txt https://example.com/upload/

# stream from stdin (pipe into PUT body)
cat data.json | curl -T - https://api.example.com/data

# upload with authentication
curl -T file.zip -u user:pass \
     https://example.com/upload/file.zip

# upload with progress bar
curl -T bigfile.iso --progress-bar \
     https://example.com/upload/bigfile.iso

# resume an interrupted upload
curl -T bigfile.iso -C - https://example.com/upload/bigfile.iso

Upload with Progress

By default curl shows a progress meter on stderr. --progress-bar switches to a simpler bar. -s (silent) hides it; add S (-sS) to still show errors. Progress info goes to stderr, so stdout (the body) stays clean — useful when piping. Use -w with %{size_upload} and %{time_total} for a one-line summary.

curl
# default progress meter (terminal)
curl -T file.zip https://example.com/upload

# simple progress bar
curl -T file.zip --progress-bar https://example.com/upload

# no progress at all (silent)
curl -T file.zip -s https://example.com/upload

# show progress but only errors (silent + show-error)
curl -T file.zip -sS https://example.com/upload

# custom progress with --write-out
curl -T file.zip -w "Uploaded %{size_upload} bytes in %{time_total}s\n" \
     -o /dev/null https://example.com/upload

# redirect progress to stderr, body to file
curl -T file.zip --progress-bar 2> progress.log \
     -o response.txt https://example.com/upload

Upload Fields & Types

Curl guesses the MIME type from the file extension. Override with ;type=. The ;filename= option changes what the server sees, useful for sanitized names. The < prefix reads a file's contents into a text field (rather than sending it as a file) — helpful when the API expects a string but you have the data in a file.

curl
# specify MIME type explicitly
curl -F "[email protected];type=text/csv" https://api.example.com/import

# let curl guess from the extension (default)
curl -F "[email protected]" https://api.example.com/upload

# force generic type
curl -F "[email protected];type=application/octet-stream" \
     https://api.example.com/upload

# send a file with a custom filename visible to server
curl -F "upload=@/tmp/abc123;filename=report.pdf;type=application/pdf" \
     https://api.example.com/upload

# send a string as if it were a file
curl -F "file=hello world" https://api.example.com/upload

# send a string as a file with a filename
curl -F "file=<inline.txt" https://api.example.com/upload
07

Authentication

Basic Auth (-u)

-u (--user) sends HTTP Basic auth (base64-encoded user:pass). Omit the password to be prompted (safer — keeps it out of shell history). --netrc reads credentials from ~/.netrc so they never appear on the command line. Basic is insecure over plain HTTP; always use HTTPS. Anyone with shell access can see -u args via ps.

curl
# HTTP Basic authentication
curl -u user:pass https://api.example.com/secret
curl --user user:pass https://api.example.com/secret

# prompt for password (don't put it on the command line)
curl -u user https://api.example.com/secret

# basic auth with explicit method (curl picks the safest by default)
curl -u user:pass --basic https://api.example.com/secret

# use a .netrc file instead of -u
echo "machine example.com login user password pass" > ~/.netrc
curl --netrc https://example.com/secret

# base64-encode credentials manually (equivalent)
curl -H "Authorization: Basic $(echo -n user:pass | base64)" \
     https://api.example.com/secret

Bearer Token

Bearer tokens (OAuth2, JWT) go in the Authorization header. Avoid putting tokens on the command line — ps exposes them to other users. Read from a file (-H @file) or environment variable, and use a headers file for sensitive values. The refresh-token snippet shows the standard OAuth2 client-credentials flow.

curl
# Bearer token (OAuth2 / JWT)
curl -H "Authorization: Bearer ACCESS_TOKEN" \
     https://api.example.com/protected

# token from an environment variable
curl -H "Authorization: Bearer $TOKEN" \
     https://api.example.com/protected

# token from a file (avoids shell history and ps)
curl -H "Authorization: Bearer $(cat ~/.token)" \
     https://api.example.com/protected

# headers from a file (cleanest for secrets)
echo "Authorization: Bearer $TOKEN" > /tmp/hdr.txt
curl -H @/tmp/hdr.txt https://api.example.com/protected
rm /tmp/hdr.txt

# refresh token flow (simplified)
TOKEN=$(curl -s -u client:secret -d "grant_type=refresh_token" \
        -d "refresh_token=$REFRESH" \
        https://auth.example.com/token | jq -r .access_token)
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/data

API Key in Header

API keys appear in custom headers (X-API-Key, X-Client-Id) or as query parameters. Query strings leak into server logs and browser history, so prefer headers. For AWS, --aws-sigv4 signs the request with your access/secret keys. GitHub uses Bearer with a personal access token (ghp_...).

curl
# API key in a custom header (varies by service)
curl -H "X-API-Key: abc123" https://api.example.com/data

# API key as a query parameter
curl "https://api.example.com/data?api_key=abc123"

# GitHub personal access token
curl -H "Authorization: Bearer ghp_TOKEN" \
     https://api.github.com/user

# AWS-style signature v4 (use the AWS CLI or aws-sigv4)
curl --aws-sigv4 "aws:amz:us-east-1:service" \
     -u "ACCESS:SECRET" https://service.amazonaws.com/

# multiple keys
curl -H "X-API-Key: abc123" \
     -H "X-Client-Id: my-app" \
     https://api.example.com/data

Netrc Files

--netrc reads credentials from ~/.netrc, mapping hosts to login/password pairs. The file must be chmod 600 or curl refuses to use it. --netrc-file points to a custom location. The 'default' entry applies to any host without a specific machine entry. This is the cleanest way to keep secrets off the command line.

curl
# ~/.netrc stores credentials per host
# format:
#   machine api.example.com
#   login alice
#   password secret
#   machine github.com
#   login alice
#   password ghp_TOKEN

# use netrc automatically
curl --netrc https://api.example.com/data

# use a custom netrc file
curl --netrc-file ~/.netrc.work https://api.example.com/data

# ignore netrc for this request
curl --netrc-optional -u user:pass https://api.example.com/data

# secure the file (required by curl on some systems)
chmod 600 ~/.netrc

# machine + default for any host
# default
# login anonymous
# password guest

Digest & NTLM

Basic sends credentials in plaintext (over HTTPS that's fine). Digest sends a hash, never the password. NTLM and Negotiate handle Windows/Kerberos SSO. --anyauth lets curl pick based on the server's WWW-Authenticate header. The server must support the method; -v shows which challenges it issues.

curl
# digest authentication (challenge-response, more secure than Basic)
curl -u user:pass --digest https://api.example.com/secret

# NTLM (Windows auth)
curl -u user:pass --ntlm https://intranet.example.com/

# negotiate (Kerberos/SPNEGO)
curl -u user:pass --negotiate https://intranet.example.com/

# anyauth: let curl pick the method
curl -u user:pass --anyauth https://api.example.com/secret

# specify the service principal for negotiate
curl -u user:pass --negotiate --service-name HTTP/intranet.example.com \
     https://intranet.example.com/

# show which method the server requested
curl -v -u user:pass --anyauth https://api.example.com 2>&1 | grep -i www-authenticate

OAuth & Tokens

OAuth2 has several grant types: client_credentials (server-to-server), authorization_code (after a browser login), password (legacy). The token endpoint returns an access_token (and often a refresh_token); use the access_token as a Bearer header for subsequent requests. App passwords (GitLab, GitHub) work with -u and bypass 2FA restrictions.

curl
# OAuth2 client credentials grant
curl -u client_id:client_secret \
     -d "grant_type=client_credentials" \
     -d "scope=read write" \
     https://auth.example.com/oauth/token

# OAuth2 authorization code (after browser redirect)
curl -d "grant_type=authorization_code" \
     -d "code=AUTH_CODE" \
     -d "redirect_uri=https://app.example.com/callback" \
     -u client_id:client_secret \
     https://auth.example.com/oauth/token

# OAuth2 password grant (deprecated but still seen)
curl -d "grant_type=password" \
     -d "username=user" -d "password=pass" \
     -u client_id:client_secret \
     https://auth.example.com/oauth/token

# use the access token
curl -H "Authorization: Bearer $ACCESS_TOKEN" \
     https://api.example.com/me

# basic auth with username and app password (e.g., GitLab)
curl -u "user:app_password" https://gitlab.example.com/api/v4/user
08

Cookies

Sending Cookies (-b)

-b (--cookie) sends cookies. A string sends it as-is; a filename (Netscape format) loads saved cookies. Multiple cookies are semicolon-separated. -b also sets the Cookie header — equivalent to -H 'Cookie: ...'. Curl does not parse Set-Cookie by default; pair -b with -c to persist new cookies.

curl
# send a single cookie
curl -b "session=abc123" https://example.com/dashboard

# send multiple cookies
curl -b "session=abc123; theme=dark; lang=en" \
     https://example.com/dashboard

# send cookies from a file (Netscape format)
curl -b cookies.txt https://example.com/dashboard

# send a cookie with attributes (path, domain)
curl -b "session=abc123; Path=/; Domain=.example.com" \
     https://example.com/

# cookie via header (manual)
curl -H "Cookie: session=abc123; theme=dark" \
     https://example.com/dashboard

Saving Cookies (-c)

-c (--cookie-jar) writes Set-Cookie response headers to a file in Netscape format. Pair -b (read) with -c (write) using the same file to maintain a session across requests: login once, then reuse the cookie jar for authenticated calls. -c - dumps to stdout for inspection.

curl
# save response cookies to a file
curl -c cookies.txt https://example.com/login

# send and save cookies in one request
curl -b cookies.txt -c cookies.txt https://example.com/dashboard

# dump cookies to stdout (Netscape format)
curl -c - https://example.com/

# inspect cookies with --dump-header
curl -D headers.txt https://example.com/
grep -i set-cookie headers.txt

# login then reuse the session cookie
curl -c cookies.txt -d "user=alice&pass=secret" \
     https://example.com/login
curl -b cookies.txt https://example.com/dashboard

Cookie Jar

A cookie jar (Netscape format) persists cookies between curl invocations, simulating a browser session. Use the same jar file for -b and -c so cookies refresh on each request. The format is tab-separated: domain, include-subdomain flag, path, secure flag, expiration (unix time), name, value. Delete the jar to log out.

curl
# Netscape cookie file format:
# domain  flag  path  secure  expiration  name  value
# .example.com  TRUE  /  FALSE  1735689600  session  abc123

# full session: login, then call API with the same jar
JAR=/tmp/cookies.txt
curl -s -c "$JAR" -d "user=alice&pass=secret" https://example.com/login
curl -s -b "$JAR" https://example.com/api/profile
curl -s -b "$JAR" https://example.com/api/settings

# update the jar on every request (refresh expiring cookies)
curl -b "$JAR" -c "$JAR" https://example.com/api/data

# expire all cookies by deleting the jar
rm "$JAR"

# view the jar contents
cat "$JAR"
# or human-readable
curl -b "$JAR" -c - -o /dev/null https://example.com/

Cookie from String

For one-off requests, -b with a string is simpler than a jar. Cookies are semicolon-separated; values with special characters should be URL-encoded. The Cookie header (via -H) is equivalent and lets you paste a browser's cookie string verbatim — useful for replaying a captured session.

curl
# one-off cookie (no file)
curl -b "session=abc123" https://example.com/

# multiple cookies in one string
curl -b "session=abc123; theme=dark; lang=en" https://example.com/

# build cookie string from variables
SID="abc123"; THEME="dark"
curl -b "session=$SID; theme=$THEME" https://example.com/

# encode a value containing special chars
curl -b "data=$(python3 -c 'import urllib.parse;print(urllib.parse.quote("a&b"))')" \
     https://example.com/

# emulate a browser cookie header exactly
curl -H "Cookie: __cfduid=abc; _ga=GA1.2.xxx; session=xyz" \
     https://example.com/

Session Management

Web logins often need a CSRF token from the login page plus cookie persistence. The pattern: GET the form (saving cookies), extract the token, POST credentials (with cookies + token), then reuse the jar for authenticated calls. Each request updates the jar (-c) so refreshed session cookies propagate. Always log out and delete the jar when done.

curl
# full login flow with cookie persistence
JAR=/tmp/session.txt

# 1. GET login page (capture CSRF token + cookies)
curl -s -c "$JAR" https://example.com/login > login.html
CSRF=$(grep csrf_token login.html | head -1 | grep -oP 'value="\K[^"]+')

# 2. POST credentials with CSRF and cookies
curl -s -b "$JAR" -c "$JAR" \
     -d "user=alice&pass=secret&csrf=$CSRF" \
     https://example.com/login

# 3. Access protected resources with the session
curl -s -b "$JAR" https://example.com/dashboard
curl -s -b "$JAR" https://example.com/api/data

# 4. Logout (invalidate session)
curl -s -b "$JAR" -c "$JAR" https://example.com/logout
rm "$JAR"
09

Proxy

HTTP/HTTPS Proxy

-x (--proxy) routes the request through a proxy. http_proxy/https_proxy/no_proxy environment variables are honored automatically. For HTTPS targets through an HTTP proxy, curl uses CONNECT to tunnel. Set no_proxy to bypass the proxy for internal hosts — comma-separated, with a leading dot meaning subdomains.

curl
# use a proxy for all requests
curl -x http://proxy.example.com:8080 https://example.com
curl --proxy http://proxy.example.com:8080 https://example.com

# HTTPS proxy (proxy connection itself is TLS)
curl -x https://proxy.example.com:8443 https://example.com

# proxy via environment variable
export https_proxy=http://proxy.example.com:8080
curl https://example.com

# proxy for a specific scheme only
curl --proxy http://proxy:8080 http://example.com
curl --proxy https://secure-proxy:8443 https://example.com

# bypass proxy for specific hosts
export no_proxy="localhost,127.0.0.1,.internal.example.com"
curl https://internal.example.com   # direct, no proxy

SOCKS Proxy

SOCKS5 is protocol-agnostic, so it tunnels HTTP, HTTPS, and more. --socks5-hostname has the proxy resolve the DNS (useful for reaching hosts your machine can't resolve). A common trick: ssh -D 1080 creates a local SOCKS proxy that tunnels traffic through a remote SSH server — great for secure browsing on untrusted networks.

curl
# SOCKS5 proxy
curl -x socks5://proxy.example.com:1080 https://example.com
curl --socks5 proxy.example.com:1080 https://example.com

# SOCKS5 with local DNS resolution (server resolves)
curl --socks5-hostname proxy.example.com:1080 https://example.com

# SOCKS4 proxy
curl --socks4 proxy.example.com:1080 https://example.com

# SSH tunnel as a SOCKS proxy (run separately)
# ssh -D 1080 user@remote -N
curl -x socks5://localhost:1080 https://example.com

# SOCKS proxy with authentication
curl -x socks5://user:[email protected]:1080 https://example.com

Proxy Authentication

Proxies often require authentication. -U (--proxy-user) provides credentials; --proxy-ntlm/--proxy-digest/--proxy-anyauth pick the scheme. Embedding user:pass in the -x URL is equivalent. For NTLM (corporate Windows proxies), --proxy-ntlm handles the multi-step handshake. -U user (no password) prompts interactively.

curl
# proxy with username/password
curl -x http://user:[email protected]:8080 https://example.com

# separate proxy auth flag
curl -x http://proxy.example.com:8080 -U user:pass https://example.com
curl --proxy-user user:pass -x http://proxy.example.com:8080 https://example.com

# prompt for proxy password
curl -U user -x http://proxy.example.com:8080 https://example.com

# NTLM-authenticated proxy
curl -U user:pass --proxy-ntlm \
     -x http://proxy.example.com:8080 https://example.com

# digest-authenticated proxy
curl -U user:pass --proxy-digest \
     -x http://proxy.example.com:8080 https://example.com

# anyauth for proxy
curl -U user:pass --proxy-anyauth \
     -x http://proxy.example.com:8080 https://example.com

No Proxy & Bypass

--noproxy (or the no_proxy env var) lists hosts that bypass the proxy. A leading dot means 'this domain and all subdomains'. '*' bypasses the proxy for everything. CIDR ranges work in recent curl. This is essential when an external proxy can't reach internal corporate hosts.

curl
# bypass proxy for specific hosts
curl --noproxy "localhost,127.0.0.1,.internal.com" \
     -x http://proxy:8080 https://example.com

# via environment variable
export no_proxy="localhost,127.0.0.1,.internal.com,10.0.0.0/8"
curl https://internal.com   # direct

# wildcard match for a domain and all subdomains
export no_proxy=".example.com"
curl https://api.example.com   # bypasses proxy

# disable proxy entirely for one request
curl --noproxy "*" -x http://proxy:8080 https://example.com

# ignore *_proxy env vars for one request
curl --noproxy "*" https://example.com

# CIDR ranges (curl 7.86+)
export no_proxy="192.168.0.0/16,10.0.0.0/8"

Proxy Headers & Tunneling

--proxy-header adds headers meant for the proxy itself (like Proxy-Authorization), not forwarded to the target. -p (--proxytunnel) forces curl to use CONNECT even for plain HTTP (normally CONNECT is only used for HTTPS). This is useful when the proxy intercepts plain HTTP and you want an end-to-end tunnel.

curl
# add a header sent only to the proxy (not the target)
curl -x http://proxy:8080 \
     --proxy-header "Proxy-Authorization: Bearer xyz" \
     https://example.com

# custom header to proxy + normal header to target
curl -x http://proxy:8080 \
     --proxy-header "X-Proxy-Tag: abc" \
     -H "X-Target-Tag: def" \
     https://example.com

# force CONNECT tunneling even for HTTP
curl -p -x http://proxy:8080 http://example.com
curl --proxytunnel -x http://proxy:8080 http://example.com

# inspect the CONNECT request to the proxy
curl -v -x http://proxy:8080 https://example.com 2>&1 | grep -i connect

# disable proxy tunneling (send HTTP directly through proxy)
curl --proxytunnel -p -x http://proxy:8080 http://example.com
10

SSL/TLS

Skip Verification (--insecure)

-k (--insecure) skips certificate verification — use only for self-signed dev certs, never in production, as it allows MITM attacks. The default verifies both the certificate chain and the hostname. --resolve lets you pin an IP while keeping the correct Host/SNI — useful for testing a specific backend behind a load balancer.

curl
# skip certificate verification (DANGEROUS)
curl -k https://self-signed.example.com
curl --insecure https://self-signed.example.com

# verify normally (default)
curl https://example.com

# show the cert chain
curl -v https://example.com 2>&1 | grep -E "SSL|cert"

# only verify the cert, not the hostname
curl --insecure https://example.com   # both off
# (no built-in flag to verify cert but skip hostname;
#  use a custom CA bundle instead)

# connect to a host with a mismatched certificate
curl -k --resolve example.com:443:1.2.3.4 https://example.com

# debug TLS handshake
curl -v https://example.com 2>&1 | grep -E "TLS|SSL|cert|cipher"

Client Certificates

Mutual TLS (mTLS) requires both a client cert (--cert) and its private key (--key). PEM is default; --cert-type P12 handles PKCS#12 bundles. --pass provides the key password. --cacert overrides the trusted CA list (default: system store) — useful for internal CAs. mTLS is common in zero-trust and service-mesh setups.

curl
# client certificate + private key (mTLS)
curl --cert client.pem --key client.key \
     https://mtls.example.com/api

# PKCS#12 bundle (combined cert + key)
curl --cert client.p12 --cert-type P12 \
     https://mtls.example.com/api

# password-protected key
curl --cert client.pem --key client.key \
     --pass secret https://mtls.example.com/api

# PEM cert with password
curl --cert client.pem:secret https://mtls.example.com/api

# specify CA bundle to verify the server
curl --cacert custom-ca.pem https://internal.example.com

# mutual TLS with custom CA
curl --cert client.pem --key client.key \
     --cacert internal-ca.pem \
     https://mtls.internal.example.com

TLS Version

--tlsv1.X sets the minimum version; --tls-max caps it. Forcing 1.2+ disables legacy protocols (SSLv3, TLS 1.0/1.1) that have known weaknesses. The default negotiates the highest mutually supported version. -v reveals the negotiated version and cipher in the 'SSL connection using' line.

curl
# force a minimum TLS version
curl --tlsv1.2 https://example.com
curl --tlsv1.3 https://example.com

# force an exact TLS version
curl --tls-max 1.2 https://example.com   # cap at 1.2
curl --tls-max 1.3 https://example.com

# negotiate the highest version both support (default)
curl https://example.com

# disable specific versions
curl --tlsv1.2 --tls-max 1.2 https://example.com   # only 1.2

# list versions curl supports
curl --version | grep -i tls

# debug which version was negotiated
curl -v https://example.com 2>&1 | grep -i "SSL connection"

Ciphers & CA Bundle

--ciphers restricts the cipher suite (OpenSSL syntax). --cacert points to a custom CA bundle for verifying servers with internal/private CAs — the system bundle (/etc/ssl/certs or macOS keychain) is used by default. If the CA bundle is outdated, servers with valid certs may be rejected; updating curl (or the ca-certificates package) fixes this.

curl
# specify allowed ciphers
curl --ciphers "AES256-GCM-SHA384:AES128-GCM-SHA256" \
     https://example.com

# use a custom CA bundle to verify the server
curl --cacert /etc/ssl/certs/internal-ca.pem \
     https://internal.example.com

# use the system CA store (default)
curl https://example.com

# use Mozilla's CA bundle (download separately)
curl --cacert cacert.pem https://example.com

# show the cipher used
curl -v https://example.com 2>&1 | grep -i cipher

# disable certificate revocation checks (CRL/OCSP)
curl --cacert ca.pem --no-clobber https://example.com

Certificate Inspection

Use -v to inspect the server's certificate (subject, issuer, validity dates). --pinnedpubkey enforces certificate pinning — the request fails if the server's public key doesn't match, defeating rogue CAs. For deep TLS debugging, --trace-ascii dumps the entire handshake. openssl s_client is a more powerful alternative for cert inspection.

curl
# show the server certificate in verbose mode
curl -v https://example.com 2>&1 | grep -A1 "server certificate"

# extract certificate details (subject, issuer, dates)
curl -v https://example.com 2>&1 | grep -E "subject|issuer|start date|expire date"

# download the server certificate (PEM)
curl -v https://example.com 2>&1 | \
  awk '/-----BEGIN CERT/,/-----END CERT/' > server.pem

# verify cert only, don't transfer body
curl -I https://example.com

# pin a specific certificate (fingerprint)
curl --pinnedpubkey sha256//BASE64= https://example.com

# check OCSP stapling
curl -v https://example.com 2>&1 | grep -i ocsp

# show full TLS trace
curl --trace-ascii - https://example.com | grep -i cert
11

Downloading

Save to File (-o/-O)

-o writes to a chosen filename; -O uses the remote filename (from the URL's basename). --output-dir sets the destination folder for -O. Multiple -O flags download multiple files in one invocation. -o /dev/null discards the body — useful for triggering webhooks or measuring timing without saving data.

curl
# save with a custom name
curl -o page.html https://example.com/index.html

# save with the remote filename
curl -O https://example.com/file.zip

# save to a specific directory
curl -O --output-dir /tmp/downloads https://example.com/file.zip

# save with remote name to a specific dir
curl -o /tmp/file.zip https://example.com/file.zip

# download multiple files with remote names
curl -O https://example.com/a.zip -O https://example.com/b.zip

# save to /dev/null (just trigger the request)
curl -o /dev/null https://example.com/webhook

Resume Downloads (-C)

-C - (--continue-at -) resumes a download from where it left off, using the existing file size as the byte offset. The server must support Range requests (check Accept-Ranges: bytes). Combine with --retry to handle flaky connections — curl retries and resumes, avoiding wasted bandwidth.

curl
# resume a partial download
curl -C - -o file.zip https://example.com/file.zip
curl --continue-at - -o file.zip https://example.com/file.zip

# resume with -O (remote name)
curl -C - -O https://example.com/file.zip

# start download at a specific byte offset
curl -C 1024 -o file.zip https://example.com/file.zip

# server must support Range requests
curl -I https://example.com/file.zip | grep -i accept-ranges

# resume an interrupted large download
curl -C - --progress-bar -O https://example.com/bigfile.iso

# combine with retry for unreliable connections
curl -C - --retry 5 --retry-all-errors -O \
     https://example.com/bigfile.iso

Progress Bar

--progress-bar shows a simpler ### bar instead of the default meter. -s silences all progress; -sS keeps errors. Progress goes to stderr, so it doesn't pollute the body on stdout — you can capture both separately. -w with %{size_download} and %{time_total} gives a clean one-line summary for scripts.

curl
# default progress meter
curl -O https://example.com/file.zip

# simple progress bar (alternative style)
curl --progress-bar -O https://example.com/file.zip

# silent (no progress, no body output)
curl -s -O https://example.com/file.zip

# silent but still show errors
curl -sS -O https://example.com/file.zip

# show only errors (no progress, body to file)
curl -sS -o file.zip https://example.com/file.zip

# custom one-line progress
curl -o file.zip -w "%{size_download} bytes in %{time_total}s\n" \
     https://example.com/file.zip

# redirect progress to a file, body to another
curl -o file.zip --progress-bar 2> progress.log \
     https://example.com/file.zip

Download Range

-r (--range) requests a byte range, returning 206 Partial Content. Useful for previewing large files, resuming, or parallel multi-connection downloads. The server must advertise Accept-Ranges: bytes. Combine with shell backgrounding (&) to download ranges concurrently, then concatenate — the basis of download accelerators like aria2.

curl
# download bytes 0-1023 (first 1KB)
curl -r 0-1023 -o part1.bin https://example.com/file.zip

# download from byte 1024 to end
curl -r 1024- -o rest.bin https://example.com/file.zip

# download last 500 bytes
curl -r -500 -o tail.bin https://example.com/file.zip

# check if server supports ranges
curl -I https://example.com/file.zip | grep -i accept-ranges
# Accept-Ranges: bytes  -> supported

# parallel range download (split a large file)
curl -r 0-49999999    -o part1.bin https://example.com/big.iso &
curl -r 50000000-99999999 -o part2.bin https://example.com/big.iso &
wait
cat part1.bin part2.bin > big.iso

# HTTP/1.1 206 Partial Content confirms success
curl -r 0-100 -v https://example.com/file.zip 2>&1 | grep HTTP

Multiple Files & Globbing

URL globbing ([1-3], {a,b,c}) issues one request per expansion. --output-dir sets where -O saves them. For true parallelism (curl itself is sequential), pipe URLs to xargs -P N to run N curls concurrently. This dramatically speeds up downloading many small files from the same host (respect the server's rate limits).

curl
# download file1.zip, file2.zip, file3.zip
curl -O https://example.com/file[1-3].zip

# download with a step (1, 3, 5, 7, 9)
curl -O https://example.com/file[1-9:2].zip

# download from multiple paths
curl -O https://example.com/{a,b,c}.zip

# download to a specific directory
curl -O --output-dir /tmp/downloads \
     https://example.com/file[1-3].zip

# sequential download (one at a time)
for url in https://example.com/{a,b,c}.zip; do
  curl -O "$url"
done

# parallel download with xargs
echo "https://example.com/{a,b,c}.zip" | tr ' ' '\n' | \
  xargs -n1 -P3 curl -O

Mirror & Recurse

Curl is single-URL by design — it doesn't recurse. To mirror a site, use wget --mirror or script link extraction with grep. For batch downloads, list URLs in a file and feed xargs -P N for parallelism. The sitemap example fetches and downloads every page indexed by a site — handy for offline snapshots.

curl
# curl doesn't recurse like wget, but you can script it
# download a page and extract its links
curl -s https://example.com/ | \
  grep -oP 'href="\K[^"]+' | grep '^http' > urls.txt

# download all linked PDFs
curl -s https://example.com/docs | \
  grep -oP 'href="\K[^"]+\.pdf' | \
  while read pdf; do curl -O "https://example.com/$pdf"; done

# use wget instead for true mirroring
# wget --mirror --convert-links --page-requisites https://example.com/

# download a list of URLs from a file
xargs -n1 -P4 curl -O < urls.txt

# fetch a sitemap and download every URL
curl -s https://example.com/sitemap.xml | \
  grep -oP '<loc>\K[^<]+' | xargs -n1 -P4 curl -O
12

Timeouts & Retries

Connection Timeout

--connect-timeout caps the time to establish the TCP/TLS connection (but not the transfer). Without it, curl may hang for minutes on unreachable hosts (OS default ~2 minutes). Always set both --connect-timeout (fail fast on dead hosts) and --max-time (cap the whole operation) in scripts to avoid hangs.

curl
# max time to establish the connection (seconds)
curl --connect-timeout 10 https://example.com

# connect timeout in milliseconds (curl 7.32+)
curl --connect-timeout 2.5 https://example.com

# connect timeout + overall max time
curl --connect-timeout 10 --max-time 30 https://example.com

# DNS resolution timeout (curl 7.88+, otherwise OS-controlled)
curl --happy-eyeballs-timeout-ms 1000 https://example.com

# fail fast on unreachable hosts
curl --connect-timeout 3 -sS https://example.com || echo "unreachable"

# combine with --retry for resilience
curl --connect-timeout 5 --max-time 20 --retry 3 \
     https://example.com

Max Time

--max-time (-m) caps the entire operation (DNS + connect + transfer). --speed-time/--speed-limit abort if transfer drops below a byte rate for a duration — perfect for stalled downloads. Exit code 28 means timeout; check $? in scripts. Always set a max-time to prevent infinite hangs on slow or stuck servers.

curl
# overall timeout for the whole operation
curl --max-time 30 https://example.com
curl -m 30 https://example.com

# max time in milliseconds (decimal)
curl --max-time 2.5 https://example.com

# max time with no data transfer (idle timeout)
curl --speed-time 30 --speed-limit 1000 https://example.com
# abort if speed < 1000 bytes/sec for 30 sec

# max time for a file upload
curl -T big.iso --max-time 600 https://example.com/upload

# max time per redirect hop
curl -L --max-time 60 https://example.com/redirect

# check exit code (28 = timed out)
curl --max-time 5 https://slow.example.com || echo "exit code: $?"

Retries

--retry retries on transient errors (timeouts, 5xx, 429) by default; --retry-all-errors extends to all failures. --retry-delay adds a fixed wait (doubled each attempt) between retries. --retry-connrefused retries even when the connection is refused (e.g., server restarting). Pair with -C - to resume partial downloads. This is essential for resilient scripts.

curl
# retry up to 3 times on transient errors
curl --retry 3 https://example.com

# retry on any error (not just transient)
curl --retry 3 --retry-all-errors https://example.com

# retry with exponential backoff
curl --retry 5 --retry-delay 2 https://example.com
# waits 2s, 4s, 6s, 8s, 10s between retries (curl 7.66+)

# retry on connection-refused specifically
curl --retry 3 --retry-connrefused https://example.com

# retry only on HTTP 5xx and 429
curl --retry 3 --retry-all-errors \
     -H "Accept: application/json" https://api.example.com

# combine with resume for large downloads
curl --retry 5 --retry-all-errors -C - -O \
     https://example.com/bigfile.iso

Retry Delay & Backoff

--retry-delay N waits N seconds before the first retry, then doubles (N, 2N, 4N...). Curl honors the server's Retry-After header for 429/503 responses. For custom logic (jitter, max total wait, condition on status code), wrap curl in a shell loop. Always pair --retry with --max-time so retries can't run forever.

curl
# fixed delay between retries
curl --retry 5 --retry-delay 2 https://example.com

# exponential backoff (curl doubles the delay each time)
curl --retry 5 --retry-delay 1 https://example.com
# waits: 1s, 2s, 4s, 8s, 16s

# honor Retry-After header (default with --retry)
curl --retry 3 https://example.com

# cap retry delay with a max-time
curl --retry 10 --retry-delay 1 --max-time 60 https://example.com

# custom backoff in a shell loop
for i in 1 2 3 4 5; do
  curl -s --max-time 10 https://example.com && break
  echo "attempt $i failed, waiting $((i*2))s..."
  sleep $((i*2))
done

# retry on specific HTTP status codes
curl --retry 5 --retry-all-errors \
     -w "%{http_code}" -o /dev/null -s https://example.com

Speed Limits

--speed-time/--speed-limit aborts a stalled transfer (no data for N seconds at a rate below the limit) — exit code 28. --limit-rate caps the transfer rate (supports K/M/G suffixes), useful for not saturating bandwidth or for testing slow networks. Combine with --retry to resume stalled downloads automatically.

curl
# abort if speed drops below 1000 bytes/sec for 30 sec
curl --speed-limit 1000 --speed-time 30 -O \
     https://example.com/bigfile.iso

# shorthand: --speed-time defaults to 30 if only --speed-limit is set
curl --speed-limit 1000 -O https://example.com/bigfile.iso

# limit max download rate (bytes/sec)
curl --limit-rate 100k -O https://example.com/file.zip

# limit rate with units (K, M, G)
curl --limit-rate 1M -O https://example.com/file.zip

# limit upload rate
curl -T big.iso --limit-rate 500k https://example.com/upload

# combine with retry on stall
curl --speed-limit 1000 --speed-time 30 \
     --retry 5 --retry-all-errors \
     -O https://example.com/bigfile.iso
13

Debugging

Verbose (-v)

-v (verbose) is the primary debugging tool. '>' lines are the request, '<' lines are the response, '*' lines are curl internals (DNS, TLS, connection) — all on stderr so stdout stays clean for the body. Redirect 2>&1 to filter with grep. Use -o /dev/null to inspect headers without saving the body.

curl
# verbose: full request + response + curl internals
curl -v https://example.com

# output goes to stderr; body to stdout
curl -v https://example.com > body.txt 2> debug.log

# only show request headers (> lines)
curl -v https://example.com 2>&1 | grep '^>'

# only show response headers (< lines)
curl -v https://example.com 2>&1 | grep '^<'

# only show curl info (* lines)
curl -v https://example.com 2>&1 | grep '^\*'

# verbose without sending the body
curl -v -o /dev/null https://example.com

Trace

--trace dumps everything (including the body) in hex + ASCII; --trace-ascii skips the hex. --trace-time adds timestamps. Unlike -v, trace shows the request and response bodies, so it's the go-to for debugging binary or compressed data. Warning: --trace logs secrets (passwords, tokens) — scrub before sharing.

curl
# trace: dump all sent and received data (hex + ASCII)
curl --trace - https://example.com

# trace ASCII only (no hex dump)
curl --trace-ascii - https://example.com

# trace to a file
curl --trace trace.log https://example.com

# trace including time per packet
curl --trace-ascii - --trace-time https://example.com

# trace with request body (hidden by default in -v)
curl --trace-ascii - -d "secret=data" https://example.com

# compare verbose vs trace
curl -v https://example.com 2>&1 | head
curl --trace-ascii - https://example.com | head

Headers Only

-I sends a HEAD request (most servers return the same headers as GET without the body). -D dumps headers to a file or '-' (stdout). To see what curl sent (request headers), use -v and grep '^>'. For just the status code, -w '%{http_code}' is the cleanest. Note: some servers respond differently to HEAD vs GET.

curl
# HEAD request: response headers only
curl -I https://example.com
curl --head https://example.com

# dump response headers to stdout, discard body
curl -D - -o /dev/null https://example.com

# dump response headers to a file
curl -D headers.txt -o body.html https://example.com

# show request headers (what curl sent)
curl -v -o /dev/null https://example.com 2>&1 | grep '^>'

# show both request and response headers
curl -v -o /dev/null https://example.com 2>&1 | grep -E '^[<>]'

# show only the status line
curl -s -o /dev/null -w "%{http_code} %{http_version}\n" \
     https://example.com
curl -D - -o /dev/null -s https://example.com | head -1

Request & Response Separation

Verbose output goes to stderr, the body to stdout, so they can be captured separately with redirection. -D saves response headers; -o saves the body; -v (stderr) captures the full conversation. This separation is essential for debugging APIs: inspect headers and body independently, log timing, and compare request vs response.

curl
# capture request and response separately
curl -v https://example.com \
     -o body.txt \
     1> /dev/null 2> verbose.log

# extract request only
curl -v https://example.com 2>&1 1>/dev/null | grep '^>' > request.txt

# extract response only
curl -v https://example.com 2>&1 1>/dev/null | grep '^<' > response.txt

# split headers and body cleanly
curl -s -D headers.txt -o body.txt https://example.com

# capture everything: body, headers, request, timing
curl -s -D headers.txt -o body.txt \
     -w "@curl-format.txt" https://example.com >> timing.log

# show body to terminal, headers to file
curl -D headers.txt https://example.com

Debug Options

--dry-run prints what curl would do without sending — great for verifying URL globbing and config. --resolve pins a host to an IP, bypassing DNS (essential for testing a specific backend). --interface binds to a network card or IP. --no-keepalive forces a fresh connection per request, useful for reproducing connection-level bugs.

curl
# show what curl would do without sending
curl --dry-run https://example.com

# dump parsed config files
curl --config /dev/null -v https://example.com 2>&1 | grep -i config

# force a fresh connection (no keep-alive reuse)
curl --no-keepalive https://example.com

# disable DNS cache
curl --doh-url https://dns.example.com/dns-query https://example.com

# use a specific network interface
curl --interface eth0 https://example.com
curl --interface 192.168.1.5 https://example.com

# resolve a hostname to a specific IP (no DNS)
curl --resolve example.com:443:1.2.3.4 https://example.com

# debug globbing (list URLs without fetching)
curl --dry-run -O https://example.com/file[1-3].zip
14

Response Info

HTTP Status Code

-w '%{http_code}' extracts just the status code for scripting. -f (--fail) makes curl exit non-zero on 4xx/5xx (without it, curl returns 0 even for 404). --fail-with-body also prints the response body on error (more useful for debugging). Use the status code to branch in scripts: 2xx success, 3xx redirect, 4xx client error, 5xx server error.

curl
# get only the status code
curl -s -o /dev/null -w "%{http_code}" https://example.com
echo   # 200

# status code with newline
curl -s -o /dev/null -w "%{http_code}\n" https://example.com

# full status line
curl -s -o /dev/null -w "%{http_version} %{http_code}\n" https://example.com

# branch on status code
CODE=$(curl -s -o /dev/null -w "%{http_code}" https://example.com)
if [ "$CODE" = "200" ]; then echo "ok"; else echo "fail: $CODE"; fi

# fail on HTTP errors (4xx, 5xx)
curl -f https://example.com || echo "HTTP error"
curl --fail-with-body https://example.com   # 7.76+: still print body

# check redirect chain status codes
curl -L -w "%{num_redirects} redirects, final %{http_code}\n" \
     -o /dev/null -s https://short.url/abc

Write-out Format

-w (--write-out) formats info about the transfer using %{variable} placeholders. Timings (time_namelookup, time_connect, time_appconnect, time_total) break down where time is spent — essential for performance analysis. %{json} outputs all variables as JSON (curl 7.70+). Store a format in a file and use -w @file for reuse.

curl
# built-in variables with -w / --write-out
curl -s -o /dev/null -w "HTTP %{http_code} in %{time_total}s\n" \
     https://example.com

# URL info
curl -s -o /dev/null -w "URL: %{url_effective}\nIP: %{remote_ip}\n" \
     https://example.com

# timing breakdown
curl -s -o /dev/null \
     -w "dns=%{time_namelookup} connect=%{time_connect} tls=%{time_appconnect} total=%{time_total}\n" \
     https://example.com

# size info
curl -s -o /dev/null \
     -w "downloaded=%{size_download} uploaded=%{size_upload}\n" \
     https://example.com

# write format from a file
cat > fmt.txt <<EOF
http_code: %{http_code}
time_total: %{time_total}s
EOF
curl -s -o /dev/null -w "@fmt.txt" https://example.com

# JSON output (curl 7.70+)
curl -s -o /dev/null -w "%{json}" https://example.com

Response Time

The timing variables decompose the request: time_namelookup (DNS), time_connect (TCP), time_appconnect (TLS), time_starttransfer (TTFB — first byte received), time_total (entire transfer). TTFB is the key metric for API latency. Compare timings across endpoints to identify bottlenecks (DNS, network, TLS, or server processing).

curl
# total time
curl -s -o /dev/null -w "%{time_total}\n" https://example.com

# DNS resolution time
curl -s -o /dev/null -w "%{time_namelookup}\n" https://example.com

# TCP connect time
curl -s -o /dev/null -w "%{time_connect}\n" https://example.com

# TLS handshake time (after connect)
curl -s -o /dev/null -w "%{time_appconnect}\n" https://example.com

# time to first byte (TTFB)
curl -s -o /dev/null -w "%{time_starttransfer}\n" https://example.com

# full timing breakdown
curl -s -o /dev/null -w \
  "dns=%{time_namelookup} tcp=%{time_connect} tls=%{time_appconnect} ttfb=%{time_starttransfer} total=%{time_total}\n" \
  https://example.com

# compare two endpoints
curl -s -o /dev/null -w "%{time_total}\n" https://api1.example.com
curl -s -o /dev/null -w "%{time_total}\n" https://api2.example.com

Extract Headers

-D - dumps response headers to stdout; pipe through grep to extract a specific header. -I (HEAD) works too but may differ from GET. -w provides %{url_effective} (final URL after redirects), %{remote_ip} (server IP), and %{num_redirects}. These are invaluable for debugging CDN routing, redirects, and server identification.

curl
# dump all response headers to stdout, discard body
curl -s -D - -o /dev/null https://example.com

# extract one header (Content-Type)
curl -s -D - -o /dev/null https://example.com | \
  grep -i "^content-type:"

# extract Content-Length (size)
curl -s -I https://example.com/file.zip | grep -i content-length

# extract Set-Cookie
curl -s -D - -o /dev/null https://example.com | grep -i set-cookie

# extract the final URL after redirects
curl -s -L -o /dev/null -w "%{url_effective}\n" https://short.url/abc

# extract the server's IP address
curl -s -o /dev/null -w "%{remote_ip}\n" https://example.com

# count redirects
curl -s -L -o /dev/null -w "%{num_redirects}\n" https://short.url/abc

Format Variables

-w (--write-out) supports dozens of variables (run 'curl -w "%{json}"' to see them all as JSON). The timing variables are the foundation of performance monitoring; the size variables track bandwidth; url_effective and num_redirects debug routing. %{json} (curl 7.70+) is the easiest way to capture everything for logging or dashboards.

curl
# complete list of common -w variables
# %{http_code}        - response status code
# %{http_version}     - HTTP version (1.1, 2, 3)
# %{url_effective}    - final URL after redirects
# %{remote_ip}        - server IP address
# %{remote_port}      - server port
# %{time_namelookup}  - DNS resolution time
# %{time_connect}     - TCP connect time
# %{time_appconnect}  - TLS handshake time
# %{time_pretransfer} - time before transfer starts
# %{time_starttransfer} - TTFB (first byte)
# %{time_total}       - total time
# %{size_download}    - bytes downloaded
# %{size_upload}      - bytes uploaded
# %{size_header}      - header bytes
# %{size_request}     - request bytes
# %{num_redirects}    - number of redirects
# %{ssl_verify_result}- SSL verification result
# %{content_type}     - response content type
# %{json}             - all variables as JSON (7.70+)

# reference: curl -w "%{json}" https://example.com
curl -s -o /dev/null -w "%{json}" https://example.com | jq
15

FTP & SMTP

FTP Basics

curl supports FTP for both downloads (GET) and uploads (PUT via -T). -u provides credentials; anonymous FTP uses 'anonymous' + an email. FTPS (--ftp-ssl) encrypts the control channel. Passive mode (default) works through NAT/firewalls; use --ftp-port for active mode if the server requires it.

curl
# anonymous FTP download
curl ftp://ftp.example.com/file.txt

# authenticated FTP
curl -u user:pass ftp://ftp.example.com/file.txt

# list a directory
curl ftp://ftp.example.com/directory/

# download a file
curl -O ftp://ftp.example.com/file.txt

# upload a file (PUT)
curl -T local.txt ftp://ftp.example.com/remote.txt -u user:pass

# use explicit FTPS (FTP over TLS)
curl --ftp-ssl ftp://ftp.example.com/file.txt -u user:pass

# passive mode (default; needed behind NAT/firewalls)
curl --ftp-pasv ftp://ftp.example.com/ -u user:pass

FTP Upload

-T uploads a file via FTP's STOR command. --ftp-create-dirs makes parent directories with MKD. --append uses APPE to add to an existing file. -C - resumes an interrupted upload. Globbing ({a,b}) uploads multiple files in one invocation. For directories, tar them first — FTP can't upload folders directly.

curl
# upload a single file
curl -T local.txt ftp://ftp.example.com/remote.txt -u user:pass

# upload with explicit filename
curl -T local.txt -o remote.txt ftp://ftp.example.com/ -u user:pass

# upload to a directory
curl -T local.txt ftp://ftp.example.com/uploads/ -u user:pass

# create a directory (MKD command)
curl --ftp-create-dirs -T local.txt \
     ftp://ftp.example.com/newdir/file.txt -u user:pass

# append to a remote file
curl -T local.txt --append ftp://ftp.example.com/remote.txt -u user:pass

# upload multiple files with globbing
curl -T "{file1,file2}.txt" ftp://ftp.example.com/ -u user:pass

# resume an interrupted upload
curl -C - -T bigfile.iso ftp://ftp.example.com/bigfile.iso -u user:pass

FTP Listing

An FTP URL ending in / lists the directory. --list-only returns just filenames. -v shows the underlying FTP commands (USER, PASS, PASV, LIST, RETR) — invaluable for debugging permission or firewall issues. Curl can't recurse into subdirectories; use wget --mirror or a script for recursive FTP downloads.

curl
# list a directory (default format)
curl ftp://ftp.example.com/ -u user:pass

# list with --list-only (names only, no details)
curl --list-only ftp://ftp.example.com/ -u user:pass

# list a specific directory
curl ftp://ftp.example.com/pub/ -u user:pass

# recursive listing (curl doesn't recurse natively)
# use --ftp-method to control PASV/EPSV behavior
curl --ftp-method nocwd ftp://ftp.example.com/ -u user:pass

# download all files in a directory (with globbing)
curl -O ftp://ftp.example.com/pub/*.txt -u user:pass

# show server responses (FTP commands)
curl -v ftp://ftp.example.com/ -u user:pass 2>&1 | grep -E "^\*|^[<>]"

SMTP Send Email

curl can send email via SMTP. The body (with headers) is in a file piped via -T. --mail-from and --mail-rcpt set the envelope (separate from the To/From headers in the body). --ssl-reqd forces STARTTLS (port 587); smtps:// uses implicit TLS (port 465). Auth via -u (LOGIN/PLAIN). Great for scripted alerts from servers without a mail client.

curl
# send email via SMTP
curl --url "smtp://smtp.example.com:587" \
     --mail-from "[email protected]" \
     --mail-rcpt "[email protected]" \
     -u "[email protected]:password" \
     -T email.txt

# email body from a file
cat > email.txt <<EOF
From: [email protected]
To: [email protected]
Subject: Test from curl

Hello from curl!
EOF
curl --url "smtp://smtp.example.com:587" \
     --mail-from "[email protected]" \
     --mail-rcpt "[email protected]" \
     -u "[email protected]:password" \
     --ssl-reqd \
     -T email.txt

# multiple recipients
curl --url "smtp://smtp.example.com:587" \
     --mail-from "[email protected]" \
     --mail-rcpt "[email protected]" \
     --mail-rcpt "[email protected]" \
     -T email.txt

# STARTTLS (port 587)
curl --ssl-reqd --url "smtp://smtp.example.com:587" \
     --mail-from "[email protected]" --mail-rcpt "[email protected]" \
     -T email.txt

# SMTPS (implicit TLS, port 465)
curl --url "smtps://smtp.example.com:465" \
     --mail-from "[email protected]" --mail-rcpt "[email protected]" \
     -T email.txt

IMAP & POP3

curl can read mail via IMAP and POP3. The URL syntax includes folder, UID, and section selectors. IMAP supports search (?UNSEEN, ?SUBJECT etc.). imaps:// and pop3s:// use implicit TLS. Note: curl is read-only for mail — it can fetch but not send via IMAP/POP3 (use SMTP for sending). The body is returned as raw RFC 822 message text.

curl
# list IMAP mailboxes
curl -u "user:pass" --url "imap://imap.example.com:143"

# list messages in INBOX
curl -u "user:pass" --url "imap://imap.example.com/INBOX"

# fetch a specific message (UID 1)
curl -u "user:pass" --url "imap://imap.example.com/INBOX;UID=1"

# fetch message headers only
curl -u "user:pass" --url "imap://imap.example.com/INBOX;UID=1;SECTION=HEADER"

# search for unread messages
curl -u "user:pass" --url "imap://imap.example.com/INBOX?UNSEEN"

# POP3: list messages
curl -u "user:pass" --url "pop3://pop.example.com/"

# POP3: retrieve message #1
curl -u "user:pass" --url "pop3://pop.example.com/1"

# use IMAPS (implicit TLS, port 993)
curl -u "user:pass" --url "imaps://imap.example.com/INBOX"

FTPS & SFTP

FTP has three encrypted variants: FTPS explicit (--ftp-ssl, upgrades on port 21), FTPS implicit (ftps://, TLS from start on port 990), and SFTP (sftp://, completely different protocol over SSH on port 22). SFTP supports key auth via --key (with --pass for encrypted keys). SFTP is generally preferred over FTPS today for its simplicity and firewall-friendliness.

curl
# FTPS: explicit TLS (upgrade to TLS on plain FTP port)
curl --ftp-ssl --ssl-reqd ftp://ftp.example.com/ -u user:pass

# FTPS: implicit TLS (TLS from the start, port 990)
curl ftps://ftp.example.com:990/ -u user:pass

# SFTP (SSH File Transfer Protocol, port 22)
curl -u user sftp://sftp.example.com/file.txt

# SFTP with key authentication
curl -u user --key ~/.ssh/id_rsa \
     sftp://sftp.example.com/file.txt

# SFTP with a password-protected key
curl -u user --key ~/.ssh/id_rsa --pass passphrase \
     sftp://sftp.example.com/file.txt

# SFTP upload
curl -u user -T local.txt sftp://sftp.example.com/remote.txt

# SFTP list directory
curl -u user sftp://sftp.example.com/
16

Parallel & Batch

Parallel Downloads

Curl itself is sequential — to download concurrently, run multiple curls in parallel via & and wait, or use xargs -P N to run N at once. This dramatically speeds up many small files. Be considerate: too many parallel connections can overwhelm the server or get you rate-limited. 4-8 concurrent is a reasonable default.

curl
# background multiple curls (shell parallelism)
curl -O https://example.com/a.zip &
curl -O https://example.com/b.zip &
curl -O https://example.com/c.zip &
wait

# parallel with xargs (4 concurrent)
echo -e "https://example.com/a.zip\nhttps://example.com/b.zip" | \
  xargs -n1 -P4 curl -O

# parallel with a URL list file
xargs -n1 -P4 curl -O < urls.txt

# zsh-specific parallel with {..}
for url in https://example.com/{a,b,c}.zip; do
  curl -O "$url" &
done; wait

# bash-specific using arrays and &
urls=("https://example.com/a.zip" "https://example.com/b.zip")
for url in "${urls[@]}"; do curl -O "$url" & done; wait

xargs Parallel

xargs -P N runs N processes concurrently, feeding each line of stdin as an argument. -a reads from a file. -I {} lets you place the URL anywhere in a command. This is the simplest way to parallelize curl downloads without extra tools. For host-aware throttling or complex retry logic, use a dedicated download manager like aria2c.

curl
# download each URL from a file, 4 concurrent
xargs -n1 -P4 -a urls.txt curl -O

# download with custom names, parallel
cat urls.txt | xargs -n2 -P4 sh -c 'curl -o "$0" "$1"'

# download and process each response
cat urls.txt | xargs -n1 -P4 -I {} sh -c \
  'curl -s {} | jq .field'

# retry failures in parallel
xargs -n1 -P4 -a urls.txt curl --retry 3 -O

# limit total concurrent connections to a host
# (xargs doesn't know about hosts; use a queue or a download manager)
xargs -n1 -P4 -a urls.txt curl -O --limit-rate 1M

# show progress for parallel downloads
xargs -n1 -P4 -a urls.txt curl -O --progress-bar 2>&1

URL Globbing

Globbing expands URLs into multiple requests: [1-10] ranges (with optional :step and zero-padding via leading zeros) and {a,b,c} lists. -o 'file#1.zip' uses #1 as the counter for custom output names. -g (--globoff) disables expansion — essential for URLs containing literal [ ] or { } (JSON, IPv6).

curl
# numeric range: 1, 2, 3
curl -O https://example.com/file[1-3].zip

# range with step: 0, 5, 10, 15, 20
curl -O https://example.com/file[0-20:5].zip

# leading zeros (zero-padded)
curl -O https://example.com/file[01-10].zip   # 01, 02, ..., 10

# list expansion: a, b, c
curl -O https://example.com/{a,b,c}.zip

# nested globs
curl -O https://example.com/{v1,v2}/file[1-2].txt

# save each with a custom name pattern
curl -o "file#1.zip" https://example.com/file[1-3].zip

# disable globbing (literal brackets)
curl -g "https://example.com/file[1].zip"

Sequential Range

For sequential downloads (when parallelism would overwhelm the server or violate rate limits), loop over URLs in bash. Add sleep for politeness. -sf (silent + fail) makes curl exit non-zero on HTTP errors so the loop can break early. This is the safe pattern for scraping or crawling: be respectful, add delays, and handle errors.

curl
# fetch URLs one at a time (no parallelism)
for i in 1 2 3 4 5; do
  curl -O "https://example.com/page$i.html"
done

# range with brace expansion (bash/zsh)
for i in {1..10}; do
  curl -s -O "https://example.com/page$i.html"
done

# with a delay between requests (rate limiting)
for i in {1..10}; do
  curl -s -O "https://example.com/page$i.html"
  sleep 1
done

# retry each URL individually
for url in $(cat urls.txt); do
  curl -s --retry 3 -O "$url"
done

# conditional: stop on first error
for url in $(cat urls.txt); do
  curl -sf -O "$url" || { echo "failed: $url"; break; }
done

GNU Parallel & curl

GNU parallel is more powerful than xargs: it preserves output order, shows progress (--bar), and handles arguments more flexibly. -j N sets concurrency. :::: reads from a file, ::: from the command line. Parallel is ideal for batch API testing, scraping, and benchmarks where you need clean, ordered output from concurrent requests.

curl
# GNU parallel: smarter than xargs (preserves order, progress)
parallel -j4 curl -O {} ::: https://example.com/a.zip \
                            https://example.com/b.zip

# parallel from a file
parallel -j4 curl -O :::: urls.txt

# parallel with progress bar
parallel --bar -j4 curl -O :::: urls.txt

# parallel with output capture
parallel -j4 'curl -s -o {}.out {}' :::: urls.txt

# parallel with retry and timing
parallel -j4 'curl -s -w "{} %{time_total}s\n" -o /dev/null {}' :::: urls.txt

# parallel POST requests
parallel -j4 'curl -s -X POST -d "id={}" https://api.example.com/{}' \
  ::: 1 2 3 4 5

# combine with --dry-run to preview
parallel --dry-run -j4 curl -O :::: urls.txt
17

Config Files

.curlrc Basics

~/.curlrc holds default options applied to every curl invocation. Use long option names without the leading --. One option per line; values can be quoted. This keeps common settings (timeouts, retries, User-Agent) consistent across scripts and interactive use. Override per-request by passing the option explicitly on the command line.

curl
# ~/.curlrc is read automatically by curl
# one option per line, long option names without the --

# ~/.curlrc example:
# show progress by default
silent
# always follow redirects
location
# default User-Agent
user-agent = "MyApp/1.0"
# default headers
header = "Accept: application/json"
# retry transient errors
retry = 3
# connect timeout
connect-timeout = 10
# max time
max-time = 30

# lines starting with # are comments
# blank lines are ignored

# test: curl https://example.com now uses all the above

Config File Location

Curl reads ~/.curlrc (or $CURL_HOME/.curlrc) by default. -K (--config) points to a specific file; -K - reads from stdin. -q (--disable) skips the config file entirely — useful when a default option breaks a specific request. CURL_HOME relocates the search directory for portable setups.

curl
# default locations (searched in order):
# 1. ~/.curlrc (or $CURL_HOME/.curlrc)
# 2. /etc/curl/curlrc (system-wide, if compiled in)

# set a custom config directory
export CURL_HOME=/etc/curl
# curl then reads $CURL_HOME/.curlrc

# specify a config file explicitly
curl -K /path/to/myconfig https://example.com
curl --config /path/to/myconfig https://example.com

# read config from stdin
echo "silent" | curl -K - https://example.com

# disable config file reading
curl -q https://example.com
curl --disable https://example.com

# check which config curl reads
curl -v https://example.com 2>&1 | grep -i config

Config with -K

-K loads options from a file, perfect for per-environment configs (dev vs prod tokens, timeouts, headers). Storing secrets in a config file (chmod 600) keeps them out of shell history and ps. Multiple -K files are processed in order, so later files override earlier ones — useful for a base config plus environment-specific overrides.

curl
# use a config file for a specific request
curl -K prod.conf https://api.example.com/data

# prod.conf:
# header = "Authorization: Bearer prod-token"
# header = "X-Environment: production"
# connect-timeout = 5
# max-time = 20

# dev.conf (different settings)
# header = "Authorization: Bearer dev-token"
# header = "X-Environment: development"
# max-time = 60

# switch environments by config file
curl -K dev.conf https://api.example.com/data
curl -K prod.conf https://api.example.com/data

# config file with URL
# echo "url = https://api.example.com/data" > req.conf
curl -K req.conf

# multiple config files (later ones override earlier)
curl -K defaults.conf -K override.conf https://example.com

Common Defaults

A solid ~/.curlrc makes every curl invocation safer and more consistent: timeouts prevent hangs, retries handle transient failures, --compressed handles gzip transparently, and a stripped User-Agent reduces fingerprinting. The write-out line logs timing and status for every request — invaluable for quick performance checks.

curl
# ~/.curlrc with sensible production defaults

# always follow redirects
location
# limit redirects
max-redirs = 5

# timeouts (fail fast, cap total)
connect-timeout = 10
max-time = 60

# retry transient failures
retry = 3
retry-delay = 2
retry-all-errors

# show errors even when silent
show-error

# auto-decompress responses
compressed

# negotiate HTTP/2
http2

# verify SSL by default (explicit for clarity)
cacert = /etc/ssl/certs/ca-certificates.crt

# don't leak the curl version
user-agent = ""

# verbose timing on failure
write-out = "time: %{time_total}s code: %{http_code}\n"

Multiple Configs

Splitting config into layered files (base, auth, api, environment) keeps things modular and secure. Process them with multiple -K flags in order — later files override earlier ones. Put secrets in a 600-permission file outside version control. A wrapper script that loads the stack lets you call 'mycurl' with all defaults applied.

curl
# base.conf — shared defaults
# silent
# show-error
# compressed
# connect-timeout = 10
# max-time = 30

# auth.conf — credentials (chmod 600)
# user = alice:secret
# header = "Authorization: Bearer TOKEN"

# api.conf — API-specific options
# header = "Accept: application/json"
# header = "X-Client: my-app/2.0"
# retry = 5

# combine configs in order (later overrides earlier)
curl -K base.conf -K auth.conf -K api.conf \
     https://api.example.com/data

# config file referencing another (nesting not supported)
# but you can source them in a wrapper script:
#   #!/bin/bash
#   curl -K base.conf -K auth.conf -K api.conf "$@"

# environment-specific overlay
curl -K base.conf -K "${ENV}.conf" https://api.example.com
18

Advanced Techniques

Connection Reuse

Within a single curl invocation, the TCP connection is reused across multiple URLs to the same host (HTTP keep-alive), saving the TLS handshake overhead. --no-keepalive disables this. --max-connects caps concurrent connections for multi-URL fetches. Across separate curl calls, there's no connection reuse — for that, use a persistent client like httpie or a script in Python/Node.

curl
# curl reuses connections within a single invocation
# multiple URLs in one command share the TCP connection
curl -o a.txt https://example.com/a \
     -o b.txt https://example.com/b

# keep-alive is on by default
# disable it for a single request
curl --no-keepalive https://example.com

# limit max connections (for multi-URL)
curl --max-connects 4 \
     -O https://example.com/a -O https://example.com/b

# persistent session across shell calls (not natively supported)
# but you can use a config file to reduce overhead
curl -K session.conf https://example.com/a
curl -K session.conf https://example.com/b

# check if connection was reused
curl -v https://example.com 2>&1 | grep -i "Connection #0"

HTTP/2 & HTTP/3

HTTP/2 multiplexes requests over one connection; HTTP/3 uses QUIC over UDP, eliminating head-of-line blocking. --http2 negotiates via ALPN over TLS; --http2-prior-knowledge skips negotiation (for cleartext h2c). --http3 requires a special curl build. Check %{http_version} to confirm what was actually used. Most modern CDNs support h2 by default.

curl
# negotiate HTTP/2 (needs TLS + ALPN)
curl --http2 https://example.com

# force HTTP/2 (fail if not available)
curl --http2-only https://example.com

# negotiate HTTP/3 (QUIC, needs curl built with quiche/openssl-quic)
curl --http3 https://example.com

# force HTTP/3 only
curl --http3-only https://example.com

# check what version was negotiated
curl -s -o /dev/null -w "%{http_version}" https://example.com
echo   # 2 or 3

# list supported HTTP versions
curl --version | grep -i -E "http2|http3"

# HTTP/2 with prior knowledge (no upgrade negotiation)
curl --http2-prior-knowledge http://example.com

Conditional Requests

Conditional requests save bandwidth: the server returns 304 Not Modified (no body) if the resource hasn't changed. -z (--time-cond) sets If-Modified-Since from a date or a local file's mtime. For ETag-based caching, set If-None-Match manually. If-Unmodified-Since enables optimistic concurrency — the PUT fails if someone else modified the resource.

curl
# fetch only if modified since a date
curl -z "Wed, 21 Oct 2025 07:28:00 GMT" \
     -O https://example.com/data.json
curl --timecond "Wed, 21 Oct 2025 07:28:00 GMT" \
     -O https://example.com/data.json

# fetch only if changed (use a local file's mtime)
curl -z local_copy.json -O https://example.com/data.json

# ETag-based conditional (If-None-Match)
curl -H 'If-None-Match: "abc123"' \
     -o data.json https://example.com/data.json
# server returns 304 Not Modified if ETag matches

# If-Modified-Since header manually
curl -H "If-Modified-Since: Wed, 21 Oct 2025 07:28:00 GMT" \
     https://example.com/data.json

# If-Unmodified-Since (for concurrency control)
curl -X PUT -H "If-Unmodified-Since: Wed, 21 Oct 2025 07:28:00 GMT" \
     -d '{"x":1}' https://api.example.com/data

Interface & DNS

--interface binds to a network card or IP — useful on multi-homed machines. --resolve pins a host to an IP, bypassing DNS and great for testing backends behind a load balancer. --doh-url uses DNS-over-HTTPS (privacy). -4/-6 force an IP family. Happy Eyeballs (default) tries both IPv4 and IPv6 to avoid IPv6 blackhole delays.

curl
# bind to a specific network interface
curl --interface eth0 https://example.com
curl --interface 192.168.1.5 https://example.com

# pin a hostname to an IP (skip DNS)
curl --resolve example.com:443:1.2.3.4 https://example.com

# pin multiple hosts
curl --resolve example.com:443:1.2.3.4 \
     --resolve api.example.com:443:1.2.3.5 \
     https://example.com/

# use a custom DNS server (DoH)
curl --doh-url https://dns.google/dns-query https://example.com
curl --doh-url https://cloudflare-dns.com/dns-query https://example.com

# use a specific DNS resolver (plain DNS, curl 7.84+)
curl --dns-servers 8.8.8.8,1.1.1.1 https://example.com

# happy eyeballs: try IPv4 and IPv6 in parallel
curl --happy-eyeballs-timeout-ms 200 https://example.com

# force IPv4 or IPv6
curl -4 https://example.com
curl -6 https://example.com

Range Requests & Partial

Range requests (-r) fetch part of a file, returning 206 Partial Content. Use cases: previewing large files, parallel multi-connection downloads (split into ranges, fetch concurrently, concatenate), and resuming interrupted transfers (-C -). The server must advertise Accept-Ranges: bytes. This is the basis of download accelerators like aria2.

curl
# request a byte range (first 1024 bytes)
curl -r 0-1023 -o part.bin https://example.com/file.zip

# request from byte 1024 to end
curl -r 1024- -o rest.bin https://example.com/file.zip

# request last 500 bytes
curl -r -500 -o tail.bin https://example.com/file.zip

# check if server supports ranges
curl -I https://example.com/file.zip | grep -i accept-ranges

# download in parallel chunks
curl -r 0-49999999    -o part1.bin https://example.com/big.iso &
curl -r 50000000-     -o part2.bin https://example.com/big.iso &
wait
cat part1.bin part2.bin > big.iso

# resume a partial download
curl -C - -o file.zip https://example.com/file.zip

# HTTP 206 = Partial Content (range succeeded)
curl -r 0-100 -v https://example.com/file.zip 2>&1 | grep HTTP

Exit Codes

Curl's exit codes distinguish failure modes: 6 (DNS), 7 (connection), 22 (HTTP error, only with -f), 28 (timeout), 35 (TLS), 60 (cert). Use these to build robust scripts that retry appropriately (e.g., retry on 28 but fail fast on 6). --fail-with-body returns the response body even on 4xx/5xx, making API error debugging far easier than -f.

curl
# common curl exit codes:
# 0  - success
# 1  - unsupported protocol
# 3  - malformed URL
# 6  - couldn't resolve host (DNS failure)
# 7  - failed to connect
# 22 - HTTP error (use with -f)
# 26 - couldn't read a file (e.g., -d @missing.txt)
# 28 - operation timeout
# 35 - SSL connect error
# 47 - too many redirects
# 52 - server didn't reply anything
# 55 - failed sending network data
# 56 - failed receiving network data
# 60 - certificate verification failed

# check exit code
curl -sf https://example.com || echo "failed: $?"

# branch on specific codes
curl --max-time 5 https://example.com
case $? in
  0)  echo "ok" ;;
  6)  echo "DNS failure" ;;
  7)  echo "connection refused" ;;
  28) echo "timeout" ;;
  *)  echo "error: $?" ;;
esac

# treat HTTP errors as failures
curl -f https://api.example.com || echo "HTTP error"

# still print body on HTTP error (7.76+)
curl --fail-with-body https://api.example.com

Was this helpful?