How to Download a File with cURL
Learn the exact cURL commands to save, resume, verify, retry, and authenticate file downloads safely.
The basic command is:
curl -L -o local-name.ext "https://example.com/path/file.ext"
-o chooses the local filename. -L follows HTTP redirects. Use -O when you want cURL to use the filename from the URL path.
This guide covers filename control, redirects, resume support, retries, authentication, verification, scripting, troubleshooting, and safer production downloads.
1. Save a download with an explicit filename
Use --output (or its short form, -o) to write the response body to a file instead of printing it to the terminal. The official cURL man page defines this option as writing output to the given file.
curl -o report.pdf "https://files.example.org/reports/latest"
Always quote URLs. Characters such as ?, &, brackets, and spaces can have a meaning to your shell.
Let cURL choose the remote filename
curl -O "https://files.example.org/reports/report-2026.pdf"
-O (or --remote-name) uses the final path component of the URL. It is useful when the URL already ends in a stable filename.
Choose a filename while following redirects
curl -L -o report.pdf "https://example.org/download"
Use -o when the URL is a redirect, a route such as /download, or a signed URL whose path does not contain the name you want.
2. Follow redirects
HTTP downloads often redirect from a short URL to a CDN or an object-storage URL. cURL does not follow redirects unless you request it.
curl -L -O "https://example.com/download"
The -L (or --location) option makes cURL repeat the request at the server’s Location target for a 3xx response. See the man page entry for --location.
Redirects and credentials
cURL initially sends credentials only to the original host. Avoid --location-trusted unless you intentionally trust every redirected host, because it can send credentials across that boundary.
3. Resume an interrupted download
To continue a partial local file, use -C - (long form --continue-at -):
curl -L -C - -o archive.tar.gz "https://example.org/archive.tar.gz"
cURL checks the existing file size and asks the server for the remaining bytes. The server must support compatible byte ranges. If it does not, restart the download instead of assuming an append is safe. The Everything curl resume documentation explains the required range behavior.
Do not resume a file that may have changed at the same URL. Use a versioned URL, an ETag, or a checksum when the publisher provides one.
4. Retry transient failures
For unreliable networks, add retries and a delay:
curl -L --retry 5 --retry-delay 2 -o file.bin "https://example.org/file.bin"
--retry handles transient errors, including timeouts and HTTP 408, 429, 500, 502, 503, 504, 522, and 524 according to the current man page. cURL applies increasing backoff between attempts.
For a script, also set a maximum transfer time and fail on HTTP errors:
curl --fail --show-error --location \
--retry 5 --retry-delay 2 --max-time 300 \
--output file.bin "https://example.org/file.bin"
--failmakes HTTP 4xx/5xx responses return a failure status.--show-errorkeeps useful diagnostics while quieting the progress meter when combined with--silent.--max-timeprevents a hung transfer from running forever.
5. Inspect headers before downloading
Use a HEAD request to inspect metadata without saving the body:
curl -I "https://example.org/file.zip"
Look for the status, Content-Type, Content-Length, ETag, Last-Modified, and Accept-Ranges. A HEAD response is not guaranteed to match every GET response, so treat it as a diagnostic step.
To see the complete redirect chain and request details:
curl -I -L "https://example.org/download"
6. Download protected files
Basic authentication
curl -L --user "$USER:$PASSWORD" \
-o private.zip "https://example.org/private.zip"
Use environment variables, a protected credential file, or the service’s token mechanism instead of putting long-lived secrets directly in shell history.
Bearer tokens and custom headers
curl -L \
-H "Authorization: Bearer $ACCESS_TOKEN" \
-o export.json "https://api.example.org/export"
Send only the headers required by the service. Never combine sensitive headers with --location-trusted unless redirected hosts are explicitly trusted.
7. Verify what you received
A successful byte transfer does not prove that the response is the expected file. Check the HTTP status and content type, then verify a size or checksum when the publisher supplies one.
curl --fail --location --output package.tar.gz "https://example.org/package.tar.gz"
sha256sum package.tar.gz
For an archive, list its contents before extracting:
tar -tzf package.tar.gz
If a server returns an HTML error page with status 200, --fail may not detect it. Inspect the file type and response headers when the source is unfamiliar.
8. Useful download options
| Need | Option | Example |
|---|---|---|
| Explicit filename | -o, --output |
curl -o report.pdf URL |
| Remote filename | -O, --remote-name |
curl -O URL |
| Follow redirects | -L, --location |
curl -L URL |
| Resume partial file | -C -, --continue-at - |
curl -C - -o file URL |
| Retry transient errors | --retry |
curl --retry 5 URL |
| Fail on HTTP errors | --fail |
curl --fail URL |
| Headers only | -I, --head |
curl -I URL |
| Set connection timeout | --connect-timeout |
curl --connect-timeout 10 URL |
| Limit total time | --max-time |
curl --max-time 300 URL |
| Show progress in scripts | --progress-bar |
curl --progress-bar -O URL |
9. Complete shell patterns
Reliable download to a temporary file
set -euo pipefail
url="https://example.org/releases/app.tar.gz"
tmp="app.tar.gz.part"
out="app.tar.gz"
curl --fail --show-error --location \
--retry 5 --retry-delay 2 \
--connect-timeout 10 --max-time 600 \
--continue-at - --output "$tmp" "$url"
mv "$tmp" "$out"
Writing to a .part file keeps an incomplete transfer from being mistaken for a finished artifact. Rename it only after cURL exits successfully.
Download several URLs
while IFS= read -r url; do
curl --fail --show-error --location --remote-name --remote-header-name "$url"
done < urls.txt
Review redirects before using --remote-header-name with untrusted sources, because a server can influence the suggested filename.
10. Python and Node.js equivalents
For one-off downloads, cURL is concise. Applications may prefer their language’s HTTP client so they can add logging, validation, and retry policies.
Python with requests
import requests
url = "https://example.org/file.zip"
with requests.get(url, stream=True, timeout=(10, 300)) as response:
response.raise_for_status()
with open("file.zip", "wb") as output:
for chunk in response.iter_content(chunk_size=1024 * 1024):
if chunk:
output.write(chunk)
Node.js with fetch
import { createWriteStream } from "node:fs";
import { pipeline } from "node:stream/promises";
const response = await fetch("https://example.org/file.zip");
if (!response.ok || !response.body) {
throw new Error(`Download failed: ${response.status}`);
}
await pipeline(response.body, createWriteStream("file.zip"));
11. Performance, reliability, and cost
- Stream large files instead of loading them into memory.
- Use a nearby CDN or object-storage endpoint when the publisher provides one.
- Resume only when the URL identifies an immutable object and the server supports ranges.
- Use bounded retries; unlimited retries can hide a persistent outage.
- Write to a temporary path and rename after validation.
- Record the URL, status, byte count, checksum, and failure reason in automation logs.
- cURL itself has no per-download service charge. Your costs come from network egress, storage, CI minutes, or the service hosting the file.
12. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Terminal fills with binary data | No output file was selected | Use -o filename or -O. |
| Downloaded file is an HTML page | Redirect, login page, or application error | Use -L, add required authentication, and inspect headers with -I -L. |
| Only a small error file is saved | HTTP error returned without --fail |
Add --fail --show-error and check the status. |
| Resume starts over or fails | Server lacks compatible byte-range support, or the object changed | Restart from scratch or use an immutable URL that supports ranges. |
| Credentials leak after redirect | Redirect crosses a host boundary | Do not use --location-trusted; inspect the redirect chain and scope credentials. |
| Transfer hangs | Slow or stalled connection | Set --connect-timeout and --max-time; use bounded retries. |
| Filename is unexpected | -O or server-provided filename differs from your expectation |
Use an explicit -o path. |
| SSL certificate error | Wrong host, expired certificate, or incomplete trust store | Fix the URL or trust store. Keep certificate verification enabled; do not use -k as a routine workaround. |
| Permission denied writing output | Directory is not writable | Choose a writable directory or fix its permissions. |
13. Or skip the browser setup
If the file you need is a website screenshot or PDF, ScreenshotNeo provides a GET endpoint instead of requiring you to install and operate a browser.
cURL:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
See the ScreenshotNeo API documentation for request options and response headers. Cookie and consent banners are accepted and removed before capture, along with more than 60 known consent platforms, newsletter popups, and chat widgets. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed; response headers identify the page verdict and whether it was billed. The service also offers an MCP server so Claude, Cursor, and other MCP clients can take screenshots, inspect pages, and capture PDFs. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000 shots.
Create a free ScreenshotNeo account and start with 1,000 screenshots per month without a card.
14. FAQ
What is the difference between -o and -O?
-o takes the exact local path you provide. -O derives the name from the URL path.
Does cURL follow redirects automatically?
No. Add -L or --location.
Can every download be resumed?
No. The server must support compatible byte-range requests, and the remote object must remain unchanged.
How do I prevent an incomplete file from being used?
Download to a temporary filename, validate the result, then rename it into place.
Should I disable TLS verification when a download fails?
No. Diagnose the certificate, hostname, or trust-store problem and keep verification enabled.


