How to Download Files With cURL: Flags, Fixes, and Scripts

Last Updated on August 11, 2026
Hand-drawn file download flowing from a terminal window into a verified folder
AI Summary
A hands-on cURL download guide covering redirects, authentication, retries, resume behavior, batch scripts, verification, and common failure modes.

I've spent a stupid number of late nights debugging scripts that were supposed to just "grab a file and move on." Nine times out of ten, the culprit was cURL doing exactly what I told it to do, not what I actually wanted. Turns out there's a big gap between "curl -O works" and "curl -O works reliably in production."

That gap is what this guide is for. cURL ships pre-installed on macOS, most Linux distros, and Windows 10 and later, which means you almost certainly already have it sitting on your machine right now. But between silent redirect failures, mystery 403s, and the jump from "download one file" to "download 500 files without melting your terminal," there's a lot of room to get stuck. I'm going to walk through the flags that matter, the step-by-step commands I actually use, the errors that trip people up most, and the point where cURL genuinely can't help you anymore — and what to reach for instead.

What Is cURL (and Why Should You Care)?

cURL is a free, open-source command-line tool for transferring data to or from a server using a URL. It speaks HTTP, HTTPS, FTP, SFTP, and a long list of other protocols, which is why it shows up everywhere from bash scripts to Dockerfiles to CI pipelines. Under the hood, the curl command you type into your terminal uses libcurl, the C transfer library that many applications and language bindings embed. PHP's cURL extension is one example; Python's popular Requests library is a separate HTTP client built on urllib3, not libcurl.

The current stable release as of writing is curl 8.21.0, released in June 2026 — though don't assume your OS ships that exact build. Distro-packaged versions of curl often lag behind the upstream project by months, sometimes longer, so it's worth running curl --version before you assume a flag like --parallel is available to you.

Why Download Files With cURL? Top Use Cases

I get asked pretty often why anyone would bother with a command-line tool when browsers can download files just fine. The honest answer: browsers are great until you need to automate anything.

Use CaseWhy cURL Shines
Downloading binaries in CI/CD pipelinesScriptable, no GUI needed
Fetching API responses or data exportsSupports custom headers, auth, and output piping
Resuming large file downloads over SSHBuilt-in resume support (-C -)
Automating recurring downloads (cron jobs)Lightweight, composable with shell scripts
Grabbing files behind authenticationFlexible auth flags (basic, token, cookies, .netrc)

A browser download is a one-off, manual click. cURL turns that same action into something you can schedule, chain into a pipeline, retry on failure, and run identically on a hundred servers at once. That's the whole appeal — it's not fancier, it's just repeatable.

Hand-drawn flow from a web source to a downloaded file and local folder

The Essential cURL Flags for Downloading Files

I keep coming back to the same dozen or so flags for 90% of what I do. Here's the cheat sheet I wish someone had handed me years ago, grouped by what they actually do.

Output and File-Saving Flags

  • -O (--remote-name) saves the file using the last part of the URL as the filename. Handy, but it can silently overwrite an existing file of the same name.
  • -o <filename> (--output) lets you pick the exact filename yourself: curl -o report.pdf https://example.com/downloads/file.pdf.
  • -J (--remote-header-name) uses the filename from the server's Content-Disposition header instead of the URL. This one's convenient for API downloads, but treat server-supplied filenames as untrusted input — download into a dedicated folder rather than your home directory, per curl's own security guidance.

Behavior Flags Every Download Needs

  • -L (--location) tells curl to follow HTTP redirects. Without it, a 3xx response gets saved as a tiny HTML redirect page instead of your actual file — this is the single most common "why is my download broken" mistake I see.
  • -C - (--continue-at -) resumes an interrupted download from where it left off.
  • -s / -S run silently but still show errors — good for scripts where you don't want a progress bar cluttering your logs.
  • --limit-rate 1M throttles bandwidth (useful on shared connections or when you don't want to hog a metered network).
  • --connect-timeout 10 and --max-time 300 stop a hung connection from freezing your script forever.
  • --retry 3 and --retry-delay 5 automatically retry on transient failures — per the curl man page, pair this with --retry-all-errors only when repeating the exact same request is genuinely safe to do.

Progress and Debugging Flags

  • -# gives you a simple progress bar instead of the default stats table.
  • -v dumps verbose output, including the full request/response headers — my go-to when something's misbehaving.
  • -I (--head) fetches just the response headers, which is a great pre-flight check before committing to a big download.
  • -w lets you print custom output after the transfer, like curl -o /dev/null -s -w "%{http_code}\n" <url> to just check a status code.

Before You Start

  • Difficulty: Beginner to Intermediate (the batch and auth sections get a bit more advanced)
  • Time Required: About 15-20 minutes to work through the core commands
  • What You'll Need: A terminal (macOS Terminal, Linux shell, or Windows PowerShell/WSL), curl installed (check with curl --version), and a test URL — I'll use a public GitHub release asset as an example since it's stable and freely accessible

How to Download Files With cURL: Step-by-Step

Step 1: Download a Single File

The absolute basics: curl -O <url> saves the file under its original name, while curl -o myfile.zip <url> lets you rename it on the way in.

curl -LO https://github.com/curl/curl/releases/download/curl-8_21_0/curl-8.21.0.tar.gz

I add -L by default now, always, no exceptions — I've been burned too many times by a redirect quietly turning my "download" into a 400-byte HTML file. You should see a progress meter tick up in your terminal, ending with the file sitting in your current directory.

When the command succeeds, the progress meter reaches 100% and curl-8.21.0.tar.gz appears in the current directory. Confirm the file before using it:

ls -lh curl-8.21.0.tar.gz

Step 2: Download and Rename the File

Use -o when you want a specific local filename instead of whatever the URL happens to end in:

curl -L -o curl-latest.tar.gz -S https://github.com/curl/curl/releases/download/curl-8_21_0/curl-8.21.0.tar.gz

The -S here re-enables error display in case you've also passed -s elsewhere in a script. This combo — -L -o <name> -S — is basically my default single-file download command.

Step 3: Resume an Interrupted Download

If a big download drops halfway through (bad wifi, VPN hiccup, whatever), don't start over. Run:

curl -C - -LO https://example.com/large-file.iso

The catch: this only works if the server honors byte-range requests. Accept-Ranges: bytes is a useful positive signal, but its absence does not prove that ranges are unsupported. The reliable check is the server's response to an actual range request: a resumable response normally returns 206 Partial Content with a valid Content-Range. Run the resume command and inspect the status with -v or -D -; if the server ignores the range or rejects the offset, restart deliberately rather than assuming the partial file is safe.

A resilient cURL download resumes after interruption and finishes with checksum verification

Step 4: Download With a Progress Bar (or Silently)

For a cleaner visual in an interactive terminal: curl -# -LO <url>. For scripts and cron jobs where you just want errors, not noise: curl -sS -LO <url>. I use the silent version almost everywhere except when I'm debugging by hand.

Step 5: Limit Download Speed

On a shared office connection (or when I don't want to be "that person" hogging bandwidth during a video call), I throttle with:

curl --limit-rate 1M -LO https://example.com/big-dataset.zip

Units are K, M, and G for kilobytes, megabytes, and gigabytes per second respectively.

Step 6: Save Response Headers Alongside the File

Sometimes I need to know exactly what the server sent back — content type, cache headers, that sort of thing — without cluttering the terminal:

curl -L -D headers.txt -o file.zip https://example.com/file.zip

This drops the response headers into headers.txt while the actual file goes into file.zip. Great for debugging content-type mismatches or verifying a CDN is actually caching what you think it's caching.

Tips & Common Pitfalls

  • Tip: Always default to -L. I genuinely can't think of a downside to including it, and I've lost hours to forgetting it.
  • Tip: When scripting, combine --fail with your download command so a non-2xx response actually causes the script to exit with an error, instead of silently saving an error page as if it were your file.
  • Pitfall: Don't pair -C - with --remove-on-error — curl documents these as incompatible, since resume needs the partial file to still be there.
  • Pitfall: -O can overwrite files without warning. If you're batch-downloading into a shared directory, use --output-dir to keep things contained.

How to Download Multiple Files and Batch Downloads With cURL

Single-file examples are the easy part. The real workflows I've built — pulling nightly data exports, syncing binaries across build servers — needed concurrency, and this is where most tutorials just... stop. There are three approaches worth knowing, each a step up in complexity.

Approach 1: Multiple URLs in One cURL Command

The simplest option is just listing URLs:

curl -LO https://example.com/a.zip -LO https://example.com/b.zip -LO https://example.com/c.zip

This works, but it's sequential — curl finishes one file completely before starting the next. Fine for three files, painful for three hundred.

Approach 2: Parallel Downloads With --parallel (curl 7.66+)

Since curl 7.66, you can add --parallel (or -Z) to fetch multiple URLs concurrently:

curl --parallel --parallel-max 5 --remote-name-all \
  https://example.com/a.zip https://example.com/b.zip https://example.com/c.zip

Worth knowing: the default parallel max is actually 50, which is way more concurrent connections than most servers (or your own network) will thank you for. I keep --parallel-max explicit and conservative — usually 4 to 8 — rather than trusting the default.

Approach 3: xargs and Bash Loops for Concurrency From a URL List

For a big list of URLs sitting in a text file, I usually reach for xargs:

cat urls.txt | xargs -n1 -P 8 curl -O -L

Or, if I want more control over what happens to each job, a background-process bash loop:

while read -r url; do
  curl -O -L "$url" &
done < urls.txt
wait

The wait at the end matters — without it, your script exits before the background downloads finish.

When to Reach for wget or aria2 Instead

I'll be straight about this: cURL isn't always the right tool. If you need to mirror an entire website directory tree, wget -r does recursive crawling out of the box in a way cURL simply wasn't built for. If you need multi-source, segmented downloads for maximum throughput on a single huge file, aria2c is genuinely faster.

ToolBest For
cURLPrecision, scripting, single-file or small-batch downloads, API interaction
wgetRecursive/mirrored site downloads, simpler bulk static-file grabs
aria2Multi-source/segmented downloads, maximizing throughput on large files

cURL's strength has always been precision and composability — piping, scripting, protocol flexibility — not brute-force crawling.

How to Download Protected Files With cURL: Authentication Patterns

Most cURL tutorials stop at -u user:pass and call it a day. That's a relic of an earlier internet. In 2026, the files I'm actually downloading come from REST APIs, session-based dashboards, and CI systems — and each of those wants a different kind of credential.

Basic Auth

curl -u username:password -O https://legacy-server.example.com/file.zip

Fine for old-school FTP servers or simple HTTP endpoints. Just know the password shows up in your shell history and process list unless you're careful — not something I'd use for anything sensitive.

Bearer / OAuth Token Auth

This is the one that's genuinely underrepresented in most guides, and it's the one I use most often now:

curl -H "Authorization: Bearer $GITHUB_TOKEN" \
  -LO https://api.github.com/repos/curl/curl/releases/assets/12345

That's a real pattern for pulling a private GitHub release asset — swap in your token and asset ID. REST APIs and OAuth2-protected resources basically all speak this language now.

Cookie-Based Session Auth

For web apps where logging in creates a session, save the cookie jar on login and reuse it on the download:

curl -c cookies.txt -d "user=me&pass=secret" https://example.com/login
curl -b cookies.txt -O https://example.com/protected/file.zip

.netrc File for Scripted and CI Environments

My preferred method for anything that runs unattended. Create a ~/.netrc file (or _netrc on Windows):

machine example.com
login myusername
password mypassword

Lock it down with chmod 600 ~/.netrc, then reference it with:

curl --netrc -LO https://example.com/protected-file.zip

The advantage is credentials never touch your shell history or script source — genuinely important in CI/CD where scripts often get logged in full.

Auth MethodFlag/OptionBest For
Basic auth-u user:passLegacy FTP, simple HTTP
Bearer token-H "Authorization: Bearer <token>"REST APIs, OAuth2
Cookie auth-b cookies.txt (+ -c to save)Session-based web apps
.netrc file--netrc or --netrc-fileCI/CD, scripted environments

Hand-drawn progress blocks, retry arrow, and completed folder

Troubleshooting Common cURL Download Failures

This is the section I actually wish existed when I was starting out, because almost nobody covers it. "Why isn't my curl download working" is a real, frequent, high-frustration search — and the fixes are usually a one-liner once you know the cause.

SymptomLikely CauseFix
curl: (60) SSL certificate problemSelf-signed or expired cert--cacert <file> or -k (dev only)
403 Forbidden / empty fileServer blocks default curl user agent-A "Mozilla/5.0..." or -H "User-Agent: ..."
Download restarts from 0 with -C -Server doesn't support RangeCheck with curl -I <url> for Accept-Ranges: bytes
0-byte file savedRedirect not followedAdd -L flag
curl: (28) Operation timed outSlow server or network issues--connect-timeout 10 --max-time 300 + --retry 3
HTML page saved instead of filePage requires JavaScript renderingcurl can't execute JS — see the section below

SSL Certificate Errors: What They Mean and How to Fix Them

Error 60 means curl couldn't verify the server's SSL certificate — usually because it's self-signed, expired, or issued by a CA curl doesn't trust. If you control the server, point curl at the right CA bundle with --cacert /path/to/ca.pem. The -k (--insecure) flag skips verification entirely, which is fine for a local dev environment and a genuinely bad idea for anything touching production or real user data.

403 Forbidden and Empty Downloads

A surprising number of servers block requests that identify themselves as curl/8.21.0 (curl's default User-Agent string), assuming they're bots or scrapers. The fix is usually just pretending to be a browser:

curl -A "Mozilla/5.0 (Windows NT 10.0; Win64; x64)" -LO https://example.com/file.zip

To check what's actually coming back before committing to a full download, I run: curl -o /dev/null -s -w "%{http_code}\n" <url>.

Timeouts, Retries, and Flaky Connections

This is the command I'd tattoo on my arm if I were braver about tattoos.

My go-to download command, the one I actually use in production scripts, layers all the reliability flags together:

curl -L -C - --retry 5 --retry-delay 3 --connect-timeout 10 --max-time 600 --fail -O <url>

That's redirect-following, resume, five retries with a 3-second delay, a 10-second connection timeout, a 10-minute overall cap, and a hard fail on bad HTTP status — basically everything I've learned the hard way to include.

cURL in Real Automation: CI/CD Pipelines, Piping, and Script Safety

Piping cURL Output to Other Tools

curl doesn't have to save anything to disk at all — piping straight into another command is one of its most underrated features:

curl -sL https://example.com/archive.tar.gz | tar xz
curl -s https://api.example.com/data | jq '.results'

Download and extract, or download and parse, in a single line. This is the pattern I use constantly for one-off data pulls.

Using cURL in GitHub Actions and CI/CD

A minimal GitHub Actions step that downloads a binary with retry logic and fails loudly on error:

- name: Download binary
  run: |
    curl -L --fail --retry 3 --retry-delay 5 \
      -o app-binary "https://example.com/releases/app-binary"

Store any tokens as CI secrets and reference them as environment variables — never hardcode them into the script itself. And use --fail (or --fail-with-body if you need to see the error body for debugging) so a broken download actually breaks the build instead of quietly succeeding with garbage.

The curl | sh Security Question

This one comes up in nearly every developer forum I've read, and for good reason: piping curl straight into sh means executing remote code you haven't looked at, based entirely on trust that the server hasn't been compromised and the connection hasn't been tampered with. That's the actual risk — not paranoia, just a straightforward supply-chain concern.

The safer pattern is to download first, inspect the script, verify a checksum or GPG signature if one's provided, and only then run it:

curl -sL https://example.com/install.sh -o install.sh
cat install.sh   # actually read it
sha256sum install.sh   # compare against published checksum if available
bash install.sh

Well-known installers like rustup and Homebrew still use the curl | sh pattern, and it's generally accepted for those specific cases because the maintainers and distribution channel are well-established. I'd still rather spend the extra ten seconds inspecting a script than find out the hard way that I shouldn't have trusted it.

When cURL Isn't Enough: JS-Rendered Pages, Anti-Bot Sites, and Structured Data

Here's a failure mode that trips up a lot of people, and it's rarely their fault: you run curl -O against what looks like a normal page, and instead of the content you expected, you get an empty HTML shell, or a Cloudflare challenge page, or something that looks like garbage. curl did exactly what it was built to do — fetch the raw HTTP response — it just can't execute JavaScript, solve a CAPTCHA, or get past an anti-bot fingerprinting system. Those aren't bugs in curl; they're outside its job description entirely.

Why cURL Fails on Modern Web Pages

Modern single-page apps often return a nearly-empty HTML skeleton, with the actual content rendered client-side by JavaScript after the page loads — something curl never executes. On top of that, systems like Cloudflare and Akamai actively serve challenge pages to anything that doesn't look like a real browser, and repeated curl requests from the same IP can get rate-limited or fingerprinted as bot traffic pretty quickly.

The Next Step: AI Scraping APIs for Developers

I'd say curl is the right tool for about 80% of file and data downloads out there — static assets, API responses, anything served as a plain HTTP resource. It's the other 20%, the JavaScript-heavy or bot-protected pages, where I've watched developers spend hours fighting headers and user-agent strings before eventually giving up and reaching for a different layer entirely.

That's genuinely the gap my team built Thunderbit to close, alongside the Chrome extension most people know us for. On the developer side, Thunderbit's Open API gives you POST /distill, which returns clean, LLM-ready Markdown from a URL — with page rendering handled by the service — and POST /extract, which returns schema-matched structured JSON when you need actual fielded data instead of readable text. There's also an MCP server so agents in Claude or Cursor can call thunderbit_distill and thunderbit_extract mid-task, and a CLI (npx @thunderbit/thunderbit-cli distill <url>) that behaves a lot like curl in your terminal. Pipe JSON output to jq, for example thunderbit distill <url> --format json | jq -r '.data.markdown'; send --format markdown output to a text tool or a file instead.

Side by side, the difference is stark. A curl request against a JS-rendered product page might return a mostly-blank <div id="root"></div>. The equivalent thunderbit distill command returns rendered page content as clean Markdown. Distill runs at 1 credit per URL and Extract at 20 credits per URL. The current endpoint-specific limits differ: Batch Distill supports up to 100 URLs per job, while Batch Extract accepts up to 50 URLs with one shared schema. Check the current API documentation before sizing a production queue.

If you're newer to the concept generally, our own explainer on what web scraping actually involves is a decent starting point, and the no-code scraping guide covers the non-developer side of this same problem for anyone on your team who isn't going to touch a terminal. For a broader comparison of tools in this space, we also put together a rundown of the best AI web scrapers worth knowing about.

Quick Reference: cURL Download Cheat Sheet

TaskCommand
Basic downloadcurl -LO <url>
Custom filenamecurl -L -o myfile.zip <url>
Resume downloadcurl -C - -LO <url>
Silent with errors showncurl -sSL -O <url>
Parallel downloadscurl --parallel --parallel-max 5 -O <url1> -O <url2>
Bearer token authcurl -H "Authorization: Bearer <token>" -LO <url>
Go-to scripted downloadcurl -LO --retry 5 --retry-delay 3 --max-time 600 --fail <url>
Pipe to extraction toolcurl -sL <url> | tar xz

Conclusion and Key Takeaways

Downloading a file with curl starts simple — curl -O and you're mostly done — but the real skill is in the layers underneath: knowing when to add -L, when to resume instead of restart, which auth pattern actually fits your workflow, and what to do the moment a 403 or a blank HTML shell shows up instead of your expected file. I've leaned on every single one of these patterns at some point, usually right after learning the hard way why it mattered.

curl remains, hands down, my default tool for straightforward file downloads and scriptable HTTP work — it's fast, it's everywhere, and it composes beautifully with the rest of a shell pipeline. But when you hit a JavaScript-rendered page or an anti-bot wall, that's not a curl problem to solve with more flags; it's a sign you need a different layer, and that's exactly where an API like Thunderbit's picks up the work without forcing you out of your terminal.

Bookmark the cheat sheet, try the retry-and-resume command on your next flaky download, and if you hit that wall where curl just returns garbage, you know what the next step looks like — Thunderbit's pricing page has the current credit breakdown if you want to see what that escalation actually costs, and our YouTube channel has walkthroughs if you'd rather watch than read.

FAQs About Downloading Files With cURL

How do I download a file with cURL and save it with a specific name?

Use -o followed by your desired filename: curl -L -o yourname.ext <url>. Add -L so redirects don't derail the download.

How do I resume a failed cURL download?

Run curl -C - -LO <url>. This only works if the server supports range requests — check first with curl -I <url> and look for Accept-Ranges: bytes in the response.

Can cURL download files that require login?

Yes, in four main ways: basic auth (-u user:pass), bearer tokens (-H "Authorization: Bearer <token>"), cookie-based sessions (-b cookies.txt), or a .netrc file for scripted environments. See the authentication section above for the full breakdown and when each one fits.

What is the difference between cURL and wget for downloading files?

cURL supports more protocols and is generally better for scripting, piping, and precise single-file or small-batch downloads. wget is built for recursive crawling and mirroring entire site directories, which makes it the better pick for bulk static-site grabs.

Why does cURL download an HTML page instead of the actual file?

Two usual suspects: you forgot the -L flag and the server redirected you somewhere else, or the page requires JavaScript to render its actual content — something curl simply can't execute. In the second case, you'll need a rendering-capable tool rather than more curl flags.

Learn More

Ke
Ke
CTO at Thunderbit | Senior Data Scientist & ML Expert With nearly a decade of experience in machine learning and data science, Ke Shen is a Columbia University alumnus and former Senior Data Scientist at Walmart Labs. With deep, peer-recognized expertise in Python, R, Java, and Statistics, he shares battle-tested insights on taking complex AI algorithms from theory to production-grade architecture.
Topics
cURL file downloadCommand-line automationReliable file transfer
Table of Contents
Thunderbit · AI web data agent

Extract data from any page in 1 click

Trusted by 250,000+ users
free plan available
Extract Data using AI
Easily transfer data to Google Sheets, Airtable, or Notion
Chrome Store Rating
PRODUCT HUNT#1 Product of the Week