Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Download files with curl and wget

By updated

Improve this page

curl and wget are the two command-line download tools of Linux. Installing a tool from a script, fetching a GitHub release, testing a REST API, downloading a database dump: everything goes through one or the other. This guide shows the concrete moves for both, and how to choose between them. Every output was measured on Debian 13 with curl 8.14.1 and wget 1.25.0.

Neither is guaranteed: the Debian 13 container image ships neither curl nor wget, the AlmaLinux 10 image only has curl. Install them with apt install curl wget or dnf install curl wget.

What you will learn

  • Download a file with curl -L -O and with wget
  • Resume an interrupted download with wget -c and curl -C -
  • Use the silent mode curl -fsSL in scripts
  • Get the URL of the latest GitHub release dynamically
  • Pass headers and authentication tokens
  • Diagnose an HTTP error with curl -v or --fail
  • Choose between curl and wget for the situation

curl vs wget: what is the difference?

Both commands speak HTTP and can save a file, but they were not designed for the same use. wget is a content grabber: it follows redirects, resumes and walks a tree without being asked. curl is a protocol client: it sends exactly the request you describe, REST methods and headers included, and adds no implicit behaviour. The table below turns this difference of philosophy into concrete options.

Capabilitycurlwget
Simple downloadYesYes
Resume a partial fileWith -C -With -c
New attempt after a network cutWith --retryBy default (20 tries)
Follow redirectsWith -LBy default
Recursive download (web mirror)NoYes
REST requests (POST, PUT, DELETE)Yes, excellentBasic
Send data read on stdinYes (-d @-, -T -)No
Installed by defaultmacOS, Windows 10 and later, AlmaLinux 10 imageRarely in a minimal image
Primary useAPI, scripting, diagnosticsSimple download, mirror

Practical rule: wget to download simply a file or a mirror. curl for scripts, APIs and HTTP diagnostics.


curl: download a file

By default, curl writes what it receives to standard output, so to your terminal. Saving to a file is an explicit choice, made with -O or -o. This logic explains most beginner surprises: without an output option, a binary archive is printed on screen, and without -L, only the redirect page is retrieved instead of the expected file.

Basic download

The file lands in the current directory, never elsewhere: -O takes the last segment of the URL as the name, ignoring the name the server suggests in its headers.

Terminal window
curl -L -O https://example.com/file.tar.gz
OptionRole
-LFollow HTTP redirects (301/302), almost always needed
-OSave with the file name from the URL
-o name.tar.gzSave under a specific name

Without -L, curl stops at the first redirect and saves the body of the redirect response, not the file: the symptom is a "file" of a few hundred bytes containing HTML.

Download with a progress bar

Without an option, curl prints a statistics table (rate, volume, remaining time) updated line by line, hard to read on a long download. --progress-bar, abbreviated -#, replaces it with a single bar. This bar can only fill up if the server announces the file size in its Content-Length header.

Terminal window
curl -L -O --progress-bar https://example.com/archive.tar.gz

Silent mode for scripts

These four letters are remembered as a block, each one answers a flaw of curl in an automated context: an HTTP error taken as a success, a progress bar that pollutes the logs, a total silence including on errors, and a redirect not followed.

Terminal window
curl -fsSL https://example.com/install.sh -o install.sh
OptionRole
-fFail: return an error code if HTTP ≥ 400 (otherwise curl prints the error body)
-sSilent: no progress bar nor statistics
-SStill show errors even in -s mode
-LFollow redirects

Resume an interrupted download

Resuming is only possible if the server accepts partial requests (Accept-Ranges header). Otherwise, curl refuses with error 33, HTTP server does not seem to support byte ranges. Cannot resume., and leaves the partial file intact (measured against a Python http.server).

Terminal window
curl -L -C - -O https://example.com/archive-10gb.tar.gz

-C - asks curl to detect automatically the resume position in the existing file: it measures its size and only requests the following bytes.


curl: fetch the latest GitHub release

Hard-coding a version number in an install script dooms it to become obsolete. The fix is to ask the server which is the current version, then build the download URL from the answer. Two sources lend themselves to this: a text file published by the project, or the GitHub Releases API, which returns JSON.

A common use case in install scripts, with the official dl.k8s.io download endpoint and the check of the binary:

Terminal window
# Latest stable kubectl version, then binary and checksum
RELEASE=$(curl -fsSL https://dl.k8s.io/release/stable.txt)
curl -fsSLO "https://dl.k8s.io/release/${RELEASE}/bin/linux/amd64/kubectl"
curl -fsSLO "https://dl.k8s.io/release/${RELEASE}/bin/linux/amd64/kubectl.sha256"
echo "$(cat kubectl.sha256) kubectl" | sha256sum -c

The old storage.googleapis.com/kubernetes-release address still answers, but its stable.txt is frozen: on 29 September 2026, it announced v1.31.0 while dl.k8s.io announced v1.37.1. A script left on the old URL installs an outdated kubectl, without the slightest error.

With the GitHub Releases API, which answers in JSON. Without authentication, it limits each IP address to 60 calls per hour: a script run in a loop from a shared CI ends up receiving a 403 error.

Terminal window
# Latest jq version
VERSION=$(curl -fsSL https://api.github.com/repos/jqlang/jq/releases/latest | grep '"tag_name"' | cut -d'"' -f4)
curl -fsSLO "https://github.com/jqlang/jq/releases/download/${VERSION}/jq-linux-amd64"

curl: pass headers and authenticate

An HTTP header is a line of metadata sent with the request: it tells the server the expected format, the caller's identity or the type of the content sent. Most APIs and private repositories refuse an anonymous request and answer 401 or 403. The three forms below cover most cases met on a server.

Custom HTTP header

-H adds a header line to the request and replaces the one curl would have sent by default if the name is identical. The option is repeatable as many times as needed in the same command. Accept: application/json asks the server for a structured response rather than an HTML page, which makes the output usable by jq.

Terminal window
curl -H "Accept: application/json" https://api.example.com/data

Bearer token (REST API)

A Bearer token is a secret string that replaces the user name / password pair: whoever holds it is authenticated. The common form goes through an environment variable, which avoids writing the secret in the script and in the shell history:

Terminal window
curl -H "Authorization: Bearer ${API_TOKEN}" https://api.example.com/resource

This variable does not protect from ps: the shell expands it before starting curl, and the full command line is visible to every account on the machine. Measured with curl 8.14.1, ps -eo args prints curl -H Authorization: Bearer s3cr3tXYZ .... On a shared server, put the header in a file readable by you only and pass it with -H @file: ps then only shows the file name.

Terminal window
printf 'Authorization: Bearer %s\n' "$API_TOKEN" > ~/.api-header
chmod 600 ~/.api-header
curl -H @"$HOME/.api-header" https://api.example.com/resource

Basic authentication

HTTP Basic authentication sends the user name / password pair encoded in base64, which is not encryption: on an http:// URL, the credentials travel in clear text. Keep this form for HTTPS connections. By omitting the password (curl -u user), curl asks for it interactively, which keeps it out of the shell history. In ps, curl itself masks the -u argument with asterisks (measured), unlike a header passed with -H.

Terminal window
curl -u user:password https://nexus.example.com/repository/artifacts/

curl: diagnose an HTTP error

When a download fails, the question to settle is whether the problem comes from the network, the certificate or the server's answer. curl -I only sends a HEAD request and prints the headers without downloading the body, ideal to check a status code or a redirect without using bandwidth. -v also details the DNS resolution, the TLS negotiation and the headers sent, each line prefixed with > for the request and < for the response.

Terminal window
# See the response headers
curl -I https://example.com/
# Verbose mode: request AND response headers
curl -v https://example.com/
# Print only the HTTP code
curl -o /dev/null -s -w "%{http_code}\n" https://example.com/
200

To test an API endpoint:

Terminal window
# POST JSON and print the formatted response
curl -s -X POST \
-H "Content-Type: application/json" \
-d '{"key": "value"}' \
https://api.example.com/endpoint | jq .

wget: download a file

wget takes the decisions curl leaves to you: it writes to a file, follows redirects and shows a progress bar without being asked. It also adds a behaviour curl only has as an option, automatic new attempts after a network cut, which explains its popularity for large files over unstable links. The downside is that it tells you less about what really happens when it fails.

Simple download

If a file of the same name already exists, wget does not overwrite it: it saves under file.tar.gz.1 (measured), a behaviour that surprises when you run a command twice. A script that then reads file.tar.gz works on the old version.

Terminal window
wget https://example.com/file.tar.gz

wget follows redirects automatically and saves with the original name.

Download under a different name

Upper-case -O imposes the destination file name, not to be confused with curl's -O, which reuses the name in the URL. A special case useful in scripts: -O - writes the content to standard output, which lets you chain it in a pipe. With several URLs, -O concatenates everything into the same file, which is rarely the desired result.

Terminal window
wget -O my-file.tar.gz https://example.com/release-1.0.tar.gz

Silent mode

-q suppresses all output, error messages included: there is no equivalent of curl's -sS that keeps errors visible. In a script, always test wget's exit code, the only indicator of a failure left. The -nv (non-verbose) variant offers a compromise: it hides the progress but keeps the errors and the summary line.

Terminal window
wget -q https://example.com/file.tar.gz

Resume an interrupted download

-c compares the size of the local file with the one announced by the server and only requests the missing bytes; without this option, wget would create a second file suffixed .1 instead of completing the first one. Against a server that does not handle partial requests, wget does not complain like curl: it downloads the file again from the start and exits with code 0 (measured).

Terminal window
wget -c https://example.com/archive-10gb.tar.gz

-c (continue) resumes where the download stopped, with a shorter option than curl's.

Limit the bandwidth

--limit-rate caps the rate of this download, expressed in bytes per second, with the suffixes k for kilobytes and m for megabytes. The limit applies to the wget instance started, not to the machine: two simultaneous downloads therefore use twice as much. It is the reflex to have before fetching a large archive from a production server whose network link is shared with users.

Terminal window
wget --limit-rate=1m https://example.com/archive.tar.gz

wget: recursive download

wget can grab a website or an FTP directory by following the links of each page. The four options below bound the recursion to the targeted directory and store the files without reproducing the server's whole tree:

Terminal window
# Recursively download a directory of a site
wget -r -np -nH --cut-dirs=1 https://example.com/docs/
# -r : recursive
# -np : no-parent (do not climb to the upper level)
# -nH : no host-name directory
# --cut-dirs=1 : remove the first path level /docs/

Compare curl and wget in practice

The three tabs below repeat the same tasks with each tool, to measure what one imposes and what the other automates. On an isolated download, the difference is cosmetic. It becomes structural in an install script, where error handling decides whether the script stops cleanly or goes on with an empty file.

Terminal window
# curl
curl -L -O https://example.com/archive.tar.gz
# wget
wget https://example.com/archive.tar.gz

Both produce the same result. wget is slightly shorter.


Check after downloading

After a download, always check the integrity of the file if the site provides a checksum. sha256sum -c recomputes the checksum of the received file and compares it with the published one: a single different byte gives FAILED and exit code 1.

Terminal window
# Download the file and its checksum
wget https://example.com/archive.tar.gz
wget https://example.com/archive.tar.gz.sha256
# Check
sha256sum -c archive.tar.gz.sha256
archive.tar.gz: OK

If the .sha256 file only holds the hash, sha256sum -c rejects it with no properly formatted checksum lines found: rebuild the line as in the kubectl example. For a complete check with GPG, see the guide Check the integrity of a file.


Common troubleshooting

A failing download rarely leaves an explicit message. The message prefix still guides the diagnosis: curl: (6) or unable to resolve host points to DNS resolution, curl: (7) to a refused connection, curl: (60) to a certificate problem. Check these three layers in order before suspecting the remote server: the failure is most often local.

SymptomLikely causeSolution
curl: (60) SSL certificate problemCertificate not recognisedFix the CA store: apt install ca-certificates or dnf install ca-certificates. curl -k turns off all checking, to be banned
curl: (33) HTTP server does not seem to support byte rangesResume asked from a server without partial requestsDelete the partial file and run again without -C -
curl: (22) The requested URL returned error: 404Wrong URL or file removed, reported thanks to -fCheck the URL and the requested version
wget: unable to resolve hostDNS not configuredping 8.8.8.8 to test connectivity, cat /etc/resolv.conf
Exit code 0 despite a 404 errorNo -f on curlAdd -f or test with curl -o /dev/null -w "%{http_code}"
Very slow downloadLimited bandwidth or saturated serverwget --limit-rate to avoid saturating the local network
wget: command not foundwget not installedapt install wget or dnf install wget

Key points

  • curl -L -O and wget download a file; wget follows redirects automatically, curl needs -L.
  • curl -fsSL is the standard combination for install scripts: fail on HTTP errors, no progress, show errors, follow redirects.
  • wget -c and curl -C - resume a partial file; on a server without partial requests, curl refuses (error 33) and wget starts again from zero.
  • A token passed with -H stays visible in ps, even from a variable: -H @file hides it.
  • Always check the checksum of a downloaded file before using it in production.
  • wget does recursion, curl handles complex REST requests.

Knowledge check

Check that the essentials of this guide stuck. The questions cover only what has just been explained here.

Knowledge check

Check what you have learned with this interactive quiz

6 questions
6 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn